From 61e2202cc22ce17f93c70b16b530ab615839f786 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 1 Aug 2025 06:46:22 -0700 Subject: [PATCH 01/37] working on new onboarding --- vite/src/App.tsx | 3 + .../src/components/general/OnboardingStep.tsx | 2 +- .../views/onboarding/components/CodeBlock.tsx | 2 +- .../onboarding-steps/CheckAccess.tsx | 20 +- .../onboarding-steps/ProductList.tsx | 2 +- vite/src/views/onboarding2/AttachProduct.tsx | 99 +++++++++ vite/src/views/onboarding2/AutumnProvider.tsx | 99 +++++++++ vite/src/views/onboarding2/Env.tsx | 24 +++ vite/src/views/onboarding2/Install.tsx | 37 ++++ vite/src/views/onboarding2/MountHandler.tsx | 193 +++++++++++++++++ .../src/views/onboarding2/OnboardingView2.tsx | 204 ++++++++++++++++++ vite/src/views/onboarding2/Step.tsx | 30 +++ 12 files changed, 703 insertions(+), 12 deletions(-) create mode 100644 vite/src/views/onboarding2/AttachProduct.tsx create mode 100644 vite/src/views/onboarding2/AutumnProvider.tsx create mode 100644 vite/src/views/onboarding2/Env.tsx create mode 100644 vite/src/views/onboarding2/Install.tsx create mode 100644 vite/src/views/onboarding2/MountHandler.tsx create mode 100644 vite/src/views/onboarding2/OnboardingView2.tsx create mode 100644 vite/src/views/onboarding2/Step.tsx diff --git a/vite/src/App.tsx b/vite/src/App.tsx index 51fca3a15..44cb10b99 100644 --- a/vite/src/App.tsx +++ b/vite/src/App.tsx @@ -19,6 +19,7 @@ import { PasswordSignIn } from "./views/auth/components/PasswordSignIn"; import { Otp } from "./views/cli/Otp"; import { AnalyticsView } from "./views/customers/customer/analytics/AnalyticsView"; import { TerminalView } from "./views/TerminalView"; +import OnboardingView2 from "./views/onboarding2/OnboardingView2"; export default function App() { return ( @@ -34,6 +35,8 @@ export default function App() { } /> } /> + } /> + } /> } /> {/* FEATURES */}
diff --git a/vite/src/views/onboarding/components/CodeBlock.tsx b/vite/src/views/onboarding/components/CodeBlock.tsx index 6d0ec3527..7b36ef44d 100644 --- a/vite/src/views/onboarding/components/CodeBlock.tsx +++ b/vite/src/views/onboarding/components/CodeBlock.tsx @@ -72,7 +72,7 @@ const CodeBlock = ({ snippets, className }: CodeBlockProps) => {
-
+
 `// app/page.tsx
 
 import { useCustomer } from "autumn-js/react";
 
 export default function CheckAccess() {
-  const { customer, allowed } = useCustomer();
+  const { customer, check } = useCustomer();
 
   const handleCheckAccess = () => {
+    const { data } = check({ ${isProduct ? "productId" : "featureId"}: "${id}" });
+    
     ${
       isProduct
         ? `// Check if customer has an active product
-    if (allowed({ productId: "${id}" })) {
+    if (data?.allowed) {
       alert("You have access to ${id}");
     } else {
       alert("You don't have access to ${id}");
     }`
         : `// Check feature balance
-    if (allowed({ featureId: "${id}" });) {
-      alert("You have access to ${id}. Balance: " + feature.balance);
+    if (data?.allowed) {
+      alert("You have access to ${id}. Balance: " + data?.balance);
     } else {
       alert("You don't have access to ${id}");
     }`
@@ -50,7 +52,7 @@ export default function CheckAccess() {
 const checkAccessCodeTypescript = (
   apiKey: string,
   id: string,
-  isProduct: boolean,
+  isProduct: boolean
 ) => `import { Autumn } from "autumn-js";
 
 const autumn = new Autumn({ secretKey: "am_sk_..." });
@@ -68,7 +70,7 @@ if (!data?.allowed) {
 const usageEventCode = (
   apiKey: string,
   id: string,
-  isProduct: boolean,
+  isProduct: boolean
 ) => `import { Autumn } from "autumn-js";
 
 const autumn = new Autumn({ secretKey: "am_sk_..." });
@@ -93,10 +95,10 @@ export default function CheckAccessStep({
 }) {
   const [isProduct, setIsProduct] = useState(true);
   const [selectedFeature, setSelectedFeature] = useState(
-    features.length > 0 ? features[0] : undefined,
+    features.length > 0 ? features[0] : undefined
   );
   const [selectedProductId, setSelectedProductId] = useState(
-    products.length > 0 ? products[0].id! : "",
+    products.length > 0 ? products[0].id! : ""
   );
 
   const selectedId = isProduct ? selectedProductId : selectedFeature?.id || "";
diff --git a/vite/src/views/onboarding/onboarding-steps/ProductList.tsx b/vite/src/views/onboarding/onboarding-steps/ProductList.tsx
index ed6664a8b..f100692cd 100644
--- a/vite/src/views/onboarding/onboarding-steps/ProductList.tsx
+++ b/vite/src/views/onboarding/onboarding-steps/ProductList.tsx
@@ -134,7 +134,7 @@ export const ProductList = ({
   );
 };
 
-const EditProductDialog = ({
+export const EditProductDialog = ({
   product,
   features,
   setProduct,
diff --git a/vite/src/views/onboarding2/AttachProduct.tsx b/vite/src/views/onboarding2/AttachProduct.tsx
new file mode 100644
index 000000000..eefb6850b
--- /dev/null
+++ b/vite/src/views/onboarding2/AttachProduct.tsx
@@ -0,0 +1,99 @@
+import Step from "./Step";
+import CodeBlock from "../onboarding/components/CodeBlock";
+import { ArrowUpRightFromSquare } from "lucide-react";
+import { Product } from "@autumn/shared";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import { useState } from "react";
+
+const attachCodeNextjs = (productId: string, apiKey: string) => {
+  return `import { useCustomer } from 'autumn-js/react';
+
+export default function PurchaseButton() {
+  const { attach } = useCustomer();
+
+  return (
+    
+  );
+}
+`;
+};
+
+export default function AttachProduct({
+  products,
+  apiKey,
+  number,
+}: {
+  products: Product[];
+  apiKey: string;
+  number: number;
+}) {
+  const [selectedProductId, setSelectedProductId] = useState(
+    products.length > 0 ? products[0].id! : ""
+  );
+
+  return (
+    
+          

+ The{" "} + + + /attach + + + {" "} + endpoint will return a Stripe Checkout URL. Once paid, the user will + be granted access to the features you defined above. +

+
+ +
+
+ } + > + + + ); +} diff --git a/vite/src/views/onboarding2/AutumnProvider.tsx b/vite/src/views/onboarding2/AutumnProvider.tsx new file mode 100644 index 000000000..9ccf8922f --- /dev/null +++ b/vite/src/views/onboarding2/AutumnProvider.tsx @@ -0,0 +1,99 @@ +import Step from "./Step"; +import CodeBlock from "../onboarding/components/CodeBlock"; +import { ArrowUpRightFromSquare } from "lucide-react"; + +const nextjs = () => { + return `// app/layout.tsx +import { AutumnProvider } from "autumn-js/react"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode, +}) { + return ( + + + + {children} + + + + ); +} +`; +}; +const vite = () => { + return `// main.tsx +import { AutumnProvider } from "autumn-js/react"; + +createRoot(document.getElementById("root")!).render( + // backendUrl is the URL of your server (eg. hono) + + + +); +`; +}; +const reactRouter = () => { + return `// root.tsx +import { AutumnProvider } from "autumn-js/react"; +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + + {children} + + + + + + ); +} +`; +}; + +export default function AutumnProviderStep({ number }: { number: number }) { + return ( + + Wrap your root layout with the AutumnProvider component, and pass in + your backend URL. Works with any React framework. +

+ } + > + +
+ ); +} diff --git a/vite/src/views/onboarding2/Env.tsx b/vite/src/views/onboarding2/Env.tsx new file mode 100644 index 000000000..265e8d851 --- /dev/null +++ b/vite/src/views/onboarding2/Env.tsx @@ -0,0 +1,24 @@ +import { CodeDisplay } from "@/components/general/CodeDisplay"; +import Step from "@/components/general/OnboardingStep"; + +import CodeBlock from "../onboarding/components/CodeBlock"; +import { ArrowUpRightFromSquare } from "lucide-react"; + +const envCode = `AUTUMN_SECRET_KEY=am_sk_1234567890`; + +export default function EnvStep() { + return ( +
+ +
+ ); +} diff --git a/vite/src/views/onboarding2/Install.tsx b/vite/src/views/onboarding2/Install.tsx new file mode 100644 index 000000000..fc217d2d4 --- /dev/null +++ b/vite/src/views/onboarding2/Install.tsx @@ -0,0 +1,37 @@ +import { CodeDisplay } from "@/components/general/CodeDisplay"; +import Step from "@/components/general/OnboardingStep"; + +import CodeBlock from "../onboarding/components/CodeBlock"; +import { ArrowUpRightFromSquare } from "lucide-react"; + +const installCode = `npm install autumn-js`; +const installCodePnpm = `pnpm install autumn-js`; +const installCodeYarn = `yarn add autumn-js`; +export default function Install() { + return ( +
+ +
+ ); +} diff --git a/vite/src/views/onboarding2/MountHandler.tsx b/vite/src/views/onboarding2/MountHandler.tsx new file mode 100644 index 000000000..aec3b98f2 --- /dev/null +++ b/vite/src/views/onboarding2/MountHandler.tsx @@ -0,0 +1,193 @@ +import Step from "./Step"; +import CodeBlock from "../onboarding/components/CodeBlock"; +import { ArrowUpRightFromSquare } from "lucide-react"; + +const nextjs = () => { + return `// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; + +export const { GET, POST } = autumnHandler({ + identify: async (request) => { + return { + customerId: "demo_user_id", // your internal customer id + customerData: { + name: "John Doe", + email: "john.doe@example.com", + }, + }; + }, +}); +`; +}; + +const remix = () => { + return `// app/routes/api.autumn.$.ts + +import { autumnHandler } from "autumn-js/remix"; +import { auth } from "../lib/auth.server"; + +export const { loader, action } = autumnHandler({ + identify: async (args) => { + return { + customerId: "demo_user_id", // your internal customer id + customerData: { + name: "John Doe", + email: "john.doe@example.com", + }, + }; + }, +}); +`; +}; + +const Tanstack = () => { + return `// routes/api/autumn.$.ts + +import { createAPIFileRoute } from "@tanstack/react-start/api"; +import { auth } from "~/lib/auth"; +import { autumnHandler } from "autumn-js/tanstack"; + +const handler = autumnHandler({ + identify: async ({ request }) => { + return { + customerId: "demo_user_id", // your internal customer id + customerData: { + name: "John Doe", + email: "john.doe@example.com", + }, + }; + }, +}); + +export const APIRoute = createAPIFileRoute("/api/autumn/$")(handler); +`; +}; + +const hono = () => { + return `//index.ts + +import { autumnHandler } from "autumn-js/hono"; + +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { + return { + customerId: "demo_user_id", // your internal customer id + customerData: { + name: "John Doe", + email: "john.doe@example.com", + }, + }; + }, + }) +); +`; +}; + +const express = () => { + return `//index.ts + +import { autumnHandler } from "autumn-js/express"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req) => { + return { + customerId: "demo_user_id", // your internal customer id + customerData: { + name: "John Doe", + email: "john.doe@example.com", + }, + }; + }, + }) +); +`; +}; + +const fastify = () => { + return `//index.ts + +import { autumnHandler } from "autumn-js/fastify"; + +fastify.route({ + method: ["GET", "POST"], + url: "/api/autumn/*", + handler: autumnHandler({ + identify: async (request) => { + return { + customerId: "demo_user_id", // your internal customer id + customerData: { + name: "John Doe", + email: "john.doe@example.com", + }, + }; + }, + }), +}); +`; +}; + +export default function MountHandler({ number }: { number: number }) { + return ( + + Mounts routes on the /api/autumn/* path which is used by Autumn's + React library. Requires an identify function that returns the + customerId for authentication. +

+ } + > + +
+ ); +} diff --git a/vite/src/views/onboarding2/OnboardingView2.tsx b/vite/src/views/onboarding2/OnboardingView2.tsx new file mode 100644 index 000000000..a0483e832 --- /dev/null +++ b/vite/src/views/onboarding2/OnboardingView2.tsx @@ -0,0 +1,204 @@ +import { useAxiosPostSWR, useAxiosSWR } from "@/services/useAxiosSwr"; +import { useEnv } from "@/utils/envUtils"; +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router"; +import { ProductsContext } from "../products/ProductsContext"; +import { PageSectionHeader } from "@/components/general/PageSectionHeader"; +import CreateProduct from "../products/CreateProduct"; +import { ProductV2 } from "@autumn/shared"; +import { ProductsTable } from "../products/ProductsTable"; +import LoadingScreen from "../general/LoadingScreen"; +import { EditProductDialog } from "../onboarding/onboarding-steps/ProductList"; +import { AutumnProvider } from "autumn-js/react"; +import PricingTable from "@/components/autumn/pricing-table"; +import Install from "./Install"; +import EnvStep from "./Env"; +import MountHandler from "./MountHandler"; +import AutumnProviderStep from "./AutumnProvider"; +import AttachProduct from "./AttachProduct"; +import SmallSpinner from "@/components/general/SmallSpinner"; +import { CustomersTable } from "../customers/CustomersTable"; +import { CustomersContext } from "../customers/CustomersContext"; + +export default function OnboardingView2() { + const env = useEnv(); + + const [apiKey, setApiKey] = useState(""); + const [showIntegrationSteps, setShowIntegrationSteps] = useState(false); + const [loading, setLoading] = useState(true); + const [entityFeatureIds, setEntityFeatureIds] = useState([]); + const [product, setProduct] = useState(null); + const [features, setFeatures] = useState([]); + const [open, setOpen] = useState(false); + const [originalProduct, setOriginalProduct] = useState(null); + + const { data, mutate, isLoading } = useAxiosSWR({ + url: `/products/data`, + env: env, + withAuth: true, + }); + const { + data: customersData, + mutate: customersMutate, + isLoading: customersIsLoading, + } = useAxiosPostSWR({ + url: `/v1/customers/all/search`, + data: { page_size: 10 }, + env: env, + withAuth: true, + }); + + useEffect(() => { + if (data) { + setFeatures(data.features); + } + }, [data]); + + if (isLoading) return ; + + return ( + +
+
+
+

Create your plans

+
+

+ Create your free and paid plans (eg. Free, Starter, Growth) + {/* products for any free plans, paid plans and any add-on or + top up products that your application offers. */} +

+
+
+ + + + {/* */} + { + await mutate(); + // setProduct(newProduct); + // setOpen(true); + }} + /> + + } + className="pr-0 border-l" + /> + { + const selectedProduct = data.products.find( + (p: ProductV2) => p.id === id + ); + setProduct(selectedProduct); + setOriginalProduct( + JSON.parse(JSON.stringify(selectedProduct)) + ); + setOpen(true); + }} + /> + +
+
+
+

Integrate Autumn

+

+ Let's integrate Autumn and get your first customer onto one of + your plans +

+
+ + +
+
+ +

+ Create a .env file in the root of your project and add the + following environment variables: +

+ +
+ + + + +

+ If you've made it to this point, you should see a customer (with + the customerId you returned in autumnHandler) here! +

+
+ + +

Watching for customers...

+
+ } + /> + + + +
+ + {/* */} +
+
+
+ + ); +} + +const StepHeader = ({ number, title }: { number: number; title: string }) => { + return ( +
+
+ {number} +
+

{title}

+
+ ); +}; + +const SamplePricingTable = () => { + return ; +}; diff --git a/vite/src/views/onboarding2/Step.tsx b/vite/src/views/onboarding2/Step.tsx new file mode 100644 index 000000000..6dc0afa05 --- /dev/null +++ b/vite/src/views/onboarding2/Step.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { cn } from "@/lib/utils"; +import { Wallet } from "lucide-react"; + +interface StepProps { + title: string; + children: React.ReactNode; + className?: string; + description?: React.ReactNode; + number?: number; +} + +function Step({ title, children, className, description, number }: StepProps) { + return ( +
+
+
+ {number || "1"} +
+ +

{title}

+
+
{description}
+ + {children} +
+ ); +} + +export default Step; From 79054ce9c2c33b87800ff9c45962795d018ff356 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 2 Aug 2025 09:01:04 -0700 Subject: [PATCH 02/37] fix: page section header products / customers --- .../components/general/PageSectionHeader.tsx | 14 ++++++++-- vite/src/views/customers/CreateCustomer.tsx | 4 +-- vite/src/views/customers/CustomersView.tsx | 4 +-- vite/src/views/products/ProductsView.tsx | 28 ++++++++++--------- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/vite/src/components/general/PageSectionHeader.tsx b/vite/src/components/general/PageSectionHeader.tsx index 98d925b63..2288f9a5d 100644 --- a/vite/src/components/general/PageSectionHeader.tsx +++ b/vite/src/components/general/PageSectionHeader.tsx @@ -8,6 +8,7 @@ export const PageSectionHeader = ({ addButton, className, classNames, + menuComponent, }: { title?: string; titleComponent?: React.ReactNode; @@ -15,16 +16,18 @@ export const PageSectionHeader = ({ isOnboarding?: boolean; addButton?: React.ReactNode; className?: string; - classNames?: { + classNames?: { title?: string; }; + menuComponent?: React.ReactNode; }) => { return (
@@ -36,7 +39,12 @@ export const PageSectionHeader = ({ {titleComponent}
{endContent} - {addButton &&
{addButton}
} +
+ {addButton &&
{addButton}
} + {menuComponent && ( +
{menuComponent}
+ )} +
); }; diff --git a/vite/src/views/customers/CreateCustomer.tsx b/vite/src/views/customers/CreateCustomer.tsx index e985a87f5..0e1da0682 100644 --- a/vite/src/views/customers/CreateCustomer.tsx +++ b/vite/src/views/customers/CreateCustomer.tsx @@ -61,10 +61,10 @@ function CreateCustomer() { return ( - + + + + + ); +} + +function PriceInformation({ + checkoutResult, + setCheckoutResult, +}: { + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; +}) { + return ( +
+ + +
+ {checkoutResult?.has_prorations && checkoutResult.lines.length > 0 && ( + + )} + +
+
+ ); +} + +function DueAmounts({ checkoutResult }: { checkoutResult: CheckoutResult }) { + const { next_cycle, product } = checkoutResult; + const nextCycleAtStr = next_cycle + ? new Date(next_cycle.starts_at).toLocaleDateString() + : undefined; + + const hasUsagePrice = product.items.some( + (item) => item.usage_model === "pay_per_use" + ); + + const showNextCycle = next_cycle && next_cycle.total !== checkoutResult.total; + + return ( +
+
+
+

Total due today

+
+ +

+ {formatCurrency({ + amount: checkoutResult?.total, + currency: checkoutResult?.currency, + })} +

+
+ {showNextCycle && ( +
+
+

Due next cycle ({nextCycleAtStr})

+
+

+ {formatCurrency({ + amount: next_cycle.total, + currency: checkoutResult?.currency, + })} + {hasUsagePrice && + usage prices} +

+
+ )} +
+ ); +} + +function ProductItems({ + checkoutResult, + setCheckoutResult, +}: { + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; +}) { + const isUpdateQuantity = + checkoutResult?.product.scenario === "active" && + checkoutResult.product.properties.updateable; + return ( +
+

Price

+ {checkoutResult?.product.items + .filter((item) => item.type !== "feature") + .map((item, index) => { + if (item.usage_model == "prepaid") { + return ( + + ); + } + + if (isUpdateQuantity) { + return null; + } + + return ( +
+

+ {item.feature ? item.feature.name : "Subscription"} +

+

+ {item.display?.primary_text} {item.display?.secondary_text} +

+
+ ); + })} +
+ ); +} + +function CheckoutLines({ checkoutResult }: { checkoutResult: CheckoutResult }) { + return ( + + + +
+

+ View details +

+ +
+
+ + {checkoutResult?.lines + .filter((line) => line.amount != 0) + .map((line, index) => { + return ( +
+

{line.description}

+

+ {new Intl.NumberFormat("en-US", { + style: "currency", + currency: checkoutResult?.currency, + }).format(line.amount)} +

+
+ ); + })} +
+
+
+ ); +} + +function CustomAccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + ); +} + +const PrepaidItem = ({ + item, + checkoutResult, + setCheckoutResult, +}: { + item: ProductItem; + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; +}) => { + const { quantity = 0, billing_units: billingUnits = 1 } = item; + const [quantityInput, setQuantityInput] = useState( + (quantity / billingUnits).toString() + ); + const { checkout } = useCustomer(); + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const scenario = checkoutResult.product.scenario; + + const handleSave = async () => { + setLoading(true); + try { + const newOptions = checkoutResult.options + .filter((option) => option.feature_id !== item.feature_id) + .map((option) => { + return { + featureId: option.feature_id, + quantity: option.quantity, + }; + }); + + newOptions.push({ + featureId: item.feature_id!, + quantity: Number(quantityInput) * billingUnits, + }); + + const { data, error } = await checkout({ + productId: checkoutResult.product.id, + options: newOptions, + }); + + if (error) { + console.error(error); + return; + } + setCheckoutResult(data!); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + setOpen(false); + } + }; + + const disableSelection = scenario === "renew"; + + return ( +
+
+

{item.feature?.name}

+ + + Qty: {quantity} + + + +
+

{item.feature?.name}

+

+ {item.display?.primary_text} {item.display?.secondary_text} +

+
+ +
+
+ setQuantityInput(e.target.value)} + /> +

+ {billingUnits > 1 && `x ${billingUnits} `} + {item.feature?.name} +

+
+ + +
+
+
+
+

+ {item.display?.primary_text} {item.display?.secondary_text} +

+
+ ); +}; + +export const PriceItem = ({ + children, + className, + ...props +}: { + children: React.ReactNode; + className?: string; +} & React.HTMLAttributes) => { + return ( +
+ {children} +
+ ); +}; + +export const PricingDialogButton = ({ + children, + size, + onClick, + disabled, + className, +}: { + children: React.ReactNode; + size?: "sm" | "lg" | "default" | "icon"; + onClick: () => void; + disabled?: boolean; + className?: string; +}) => { + return ( + + ); +}; diff --git a/vite/src/components/autumn/pricing-table.tsx b/vite/src/components/autumn/pricing-table.tsx index 7517353fd..7d1aa2298 100644 --- a/vite/src/components/autumn/pricing-table.tsx +++ b/vite/src/components/autumn/pricing-table.tsx @@ -1,34 +1,37 @@ import React from "react"; -import { useCustomer, usePricingTable } from "autumn-js/react"; + +import { useCustomer, usePricingTable, ProductDetails } from "autumn-js/react"; import { createContext, useContext, useState } from "react"; import { cn } from "@/lib/utils"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; -import { Check, Loader2 } from "lucide-react"; -import AttachDialog from "@/components/autumn/attach-dialog"; +import CheckoutDialog from "@/components/autumn/checkout-dialog"; import { getPricingTableContent } from "@/lib/autumn/pricing-table-content"; -import { Product, ProductItem } from "autumn-js"; +import type { Product, ProductItem } from "autumn-js"; +import { Loader2 } from "lucide-react"; export default function PricingTable({ productDetails, + products, }: { - productDetails?: any; + productDetails?: ProductDetails[]; + products?: Product[]; }) { - const { attach } = useCustomer(); + const { checkout } = useCustomer(); const [isAnnual, setIsAnnual] = useState(false); - const { products, isLoading, error } = usePricingTable({ productDetails }); + const { isLoading, error } = usePricingTable({ productDetails }); - if (isLoading) { - return ( -
- -
- ); - } + // if (isLoading) { + // return ( + //
+ // + //
+ // ); + // } - if (error) { - return
Something went wrong...
; - } + // if (error) { + // return
Something went wrong...
; + // } const intervals = Array.from( new Set( @@ -38,7 +41,7 @@ export default function PricingTable({ const multiInterval = intervals.length > 1; - const intervalFilter = (product: any) => { + const intervalFilter = (product: Product) => { if (!product.properties?.interval_group) { return true; } @@ -55,10 +58,10 @@ export default function PricingTable({ }; return ( -
+
{products && ( { if (product.id) { - await attach({ + await checkout({ productId: product.id, - dialog: AttachDialog, - openInNewTab: true, - successUrl: window.location.href, + dialog: CheckoutDialog, }); } else if (product.display?.button_url) { window.open(product.display?.button_url, "_blank"); @@ -162,7 +165,8 @@ export const PricingTableContainer = ({ )}
@@ -194,9 +198,10 @@ export const PricingCard = ({ throw new Error(`Product with id ${productId} not found`); } - const { name, display: productDisplay, items } = product; + const { name, display: productDisplay } = product; const { buttonText } = getPricingTableContent(product); + const isRecommended = productDisplay?.recommend_text ? true : false; const mainPriceDisplay = product.properties?.is_free ? { @@ -211,7 +216,7 @@ export const PricingCard = ({ return (
-

- {productDisplay?.name || name} +

+ {productDisplay?.name || name || ( + + Name this product + + )}

{productDisplay?.description && (
@@ -239,7 +248,7 @@ export const PricingCard = ({ )}
-

+

{mainPriceDisplay?.primary_text}{" "} {mainPriceDisplay?.secondary_text && ( @@ -255,7 +264,6 @@ export const PricingCard = ({
@@ -266,7 +274,7 @@ export const PricingCard = ({ recommended={productDisplay?.recommend_text ? true : false} {...buttonProps} > - {buttonText} + {productDisplay?.button_text || buttonText}

@@ -277,12 +285,10 @@ export const PricingCard = ({ // Pricing Feature List export const PricingFeatureList = ({ items, - showIcon = true, everythingFrom, className, }: { items: ProductItem[]; - showIcon?: boolean; everythingFrom?: string; className?: string; }) => { @@ -293,10 +299,7 @@ export const PricingFeatureList = ({ )}
{items.map((item, index) => ( -
- {showIcon && ( - - )} +
{item.display?.primary_text} {item.display?.secondary_text && ( diff --git a/vite/src/components/general/ToggleButton.tsx b/vite/src/components/general/ToggleButton.tsx index ce6cf2eb1..369f1698c 100644 --- a/vite/src/components/general/ToggleButton.tsx +++ b/vite/src/components/general/ToggleButton.tsx @@ -26,7 +26,11 @@ export const ToggleButton = ({ */} - { - await mutate(); - // setProduct(newProduct); - // setOpen(true); - }} - /> - - } - className="pr-0 border-l" - /> - { - const selectedProduct = data.products.find( - (p: ProductV2) => p.id === id - ); - setProduct(selectedProduct); - setOriginalProduct( - JSON.parse(JSON.stringify(selectedProduct)) - ); - setOpen(true); - }} - /> - -
-

Integrate Autumn

@@ -175,15 +122,9 @@ export default function OnboardingView2() {

- - {/* */}
-
+
*/} ); } diff --git a/vite/src/views/onboarding2/integrate/AITools.tsx b/vite/src/views/onboarding2/integrate/AITools.tsx new file mode 100644 index 000000000..6322f8cab --- /dev/null +++ b/vite/src/views/onboarding2/integrate/AITools.tsx @@ -0,0 +1,74 @@ +import { StepHeader } from "./StepHeader"; +import { Button } from "@/components/ui/button"; +import CopyButton from "@/components/general/CopyButton"; +import { ExternalLink, Download } from "lucide-react"; +import { toast } from "sonner"; + +export const AITools = () => { + // MCP configuration for Autumn + const mcpConfig = { + name: "autumn", + command: "npx", + args: ["-y", "mcp-remote", "https://docs.useautumn.com/mcp"], + }; + + // Base64 encode the configuration for Cursor's install URL + const encodedConfig = btoa(JSON.stringify(mcpConfig)); + const cursorInstallUrl = `https://cursor.com/install-mcp?name=autumn&config=${encodedConfig}`; + + // Manual JSON configuration for copy-paste + const manualConfig = JSON.stringify( + { + mcpServers: { + autumn: mcpConfig, + }, + }, + null, + 2 + ); + + const handleCursorInstall = () => { + window.open(cursorInstallUrl, "_blank"); + }; + + return ( +
+ +

+ If you're using Cursor or Claude Code, you can install our MCP server to + use AI to integrate Autumn. +

+ + {/* One-click install for Cursor */} +
+ + +
+
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx new file mode 100644 index 000000000..3dfe12419 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx @@ -0,0 +1,66 @@ +import { AITools } from "./AITools"; +import { SelectStack } from "./SelectStack"; +import { useState } from "react"; +import { IntegrateContext } from "./IntegrateContext"; +import { notNullish } from "@/utils/genUtils"; +import { Install } from "./integration-steps/Install"; +import { AutumnHandler } from "./integration-steps/AutumnHandler"; +import { parseAsString, parseAsJson, useQueryStates } from "nuqs"; + +export default function IntegrateAutumn() { + const [stack, setStack] = useState<{ + frontend: string; + backend: string; + auth: string; + customerType: string; + }>({ + frontend: "", + backend: "", + auth: "", + customerType: "", + }); + + const [queryStates, setQueryStates] = useQueryStates({ + frontend: parseAsString.withDefault(""), + backend: parseAsString.withDefault(""), + auth: parseAsString.withDefault(""), + customerType: parseAsString.withDefault(""), + }); + + const stackSelected = Object.values(stack).every(notNullish); + + return ( + +
+
+
+

Integrate Autumn

+

+ Let's integrate Autumn and get your first customer onto one of + your plans +

+
+ +
+ + + {stackSelected && ( + <> + + + + )} +
+ {/*
+ +

+ Create a .env file in the root of your project and add the following + environment variables: +

+ +
*/} +
+
+
+ ); +} diff --git a/vite/src/views/onboarding2/integrate/IntegrateContext.tsx b/vite/src/views/onboarding2/integrate/IntegrateContext.tsx new file mode 100644 index 000000000..8f16ae543 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/IntegrateContext.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +export const IntegrateContext = createContext(null); + +export const useIntegrateContext = () => { + const context = useContext(IntegrateContext); + + if (context === undefined) { + throw new Error( + "useProductContext must be used within a ProductContextProvider" + ); + } + + return context; +}; diff --git a/vite/src/views/onboarding2/integrate/SelectStack.tsx b/vite/src/views/onboarding2/integrate/SelectStack.tsx new file mode 100644 index 000000000..a28cff3e2 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/SelectStack.tsx @@ -0,0 +1,183 @@ +import { useState } from "react"; +import { StepHeader } from "./StepHeader"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useIntegrateContext } from "./IntegrateContext"; + +export const SelectStack = () => { + const { queryStates, setQueryStates } = useIntegrateContext(); + + const frontendOptions = [ + { value: "nextjs", label: "Next.js" }, + { value: "vite", label: "Vite SPA" }, + { value: "tanstack", label: "Tanstack Start" }, + { value: "non-react", label: "Non-React" }, + ]; + + const backendOptions = [ + { value: "nextjs", label: "Next.js" }, + { value: "react_router", label: "React Router 7" }, + { value: "hono", label: "Hono" }, + { value: "express", label: "Express" }, + { value: "elysia", label: "Elysia" }, + { value: "supabase", label: "Supabase" }, + { value: "convex", label: "Convex" }, + ]; + + const authOptions = [ + { value: "better_auth", label: "Better Auth" }, + { value: "supabase", label: "Supabase Auth" }, + { value: "clerk", label: "Clerk" }, + { value: "other", label: "Other" }, + ]; + + const customerOptions = [ + { value: "user", label: "Users" }, + { value: "org", label: "Organizations" }, + // { value: "other", label: "Other (eg. Projects / Workspaces)" }, + ]; + + return ( +
+ +

+ Help us customize the integration guide for your specific tech stack. +

+ +
+ {/* Frontend Framework */} +
+ + +
+ + {/* Backend Framework */} +
+ + +
+ + {/* Auth Provider */} +
+ + +
+ + {/* Customer Type */} +
+ + +
+
+ + {/* {(stack.frontend || + stack.backend || + stack.auth || + stack.customerType) && ( +
+

Selected Stack:

+
+ {stack.frontend && ( +

+ Frontend:{" "} + {frontendOptions.find((f) => f.value === stack.frontend)?.label} +

+ )} + {stack.backend && ( +

+ Backend:{" "} + {backendOptions.find((b) => b.value === stack.backend)?.label} +

+ )} + {stack.auth && ( +

+ Auth:{" "} + {authOptions.find((a) => a.value === stack.auth)?.label} +

+ )} + {stack.customerType && ( +

+ Customers:{" "} + { + customerOptions.find((c) => c.value === stack.customerType) + ?.label + } +

+ )} +
+
+ )} */} +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/StepHeader.tsx b/vite/src/views/onboarding2/integrate/StepHeader.tsx new file mode 100644 index 000000000..8ac04206d --- /dev/null +++ b/vite/src/views/onboarding2/integrate/StepHeader.tsx @@ -0,0 +1,16 @@ +export const StepHeader = ({ + number, + title, +}: { + number: number; + title: string; +}) => { + return ( +
+
+ {number} +
+

{title}

+
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx new file mode 100644 index 000000000..6346ba3fb --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx @@ -0,0 +1,137 @@ +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { StepHeader } from "../StepHeader"; +import { useIntegrateContext } from "../IntegrateContext"; + +import {} from "./handlerSnippets"; +import { + nextjsBetterAuthOrg, + nextjsBetterAuthUser, + nextjsClerkOrg, + nextjsClerkUser, + nextjsOther, + nextjsSupabaseOrg, + nextjsSupabaseUser, +} from "./snippets/nextjsHandler"; +import { rr7BetterAuth, rr7Clerk, rr7Supabase } from "./snippets/rr7Handler"; +import { + honoBetterAuth, + honoClerk, + honoSupabase, +} from "./snippets/honoHandler"; +import { + expressBetterAuth, + expressClerk, + expressSupabase, +} from "./snippets/expressHandler"; +import { elysiaBetterAuth } from "./snippets/elysiaHandler"; + +const snippet = () => { + return { + nextjs: { + ["better_auth"]: { + user: nextjsBetterAuthUser, + org: nextjsBetterAuthOrg, + }, + ["supabase"]: { + user: nextjsSupabaseUser, + org: nextjsSupabaseOrg, + }, + ["clerk"]: { + user: nextjsClerkUser, + org: nextjsClerkOrg, + }, + ["other"]: { + user: nextjsOther, + org: nextjsOther, + }, + }, + ["react_router"]: { + ["better_auth"]: { + user: rr7BetterAuth("user"), + org: rr7BetterAuth("org"), + }, + ["supabase"]: { + user: rr7Supabase("user"), + org: rr7Supabase("org"), + }, + ["clerk"]: { + user: rr7Clerk("user"), + org: rr7Clerk("org"), + }, + }, + ["hono"]: { + ["clerk"]: { + user: honoClerk("user"), + org: honoClerk("org"), + }, + ["supabase"]: { + user: honoSupabase("user"), + org: honoSupabase("org"), + }, + ["better_auth"]: { + user: honoBetterAuth("user"), + org: honoBetterAuth("org"), + }, + }, + ["express"]: { + ["better_auth"]: { + user: expressBetterAuth("user"), + org: expressBetterAuth("org"), + }, + ["clerk"]: { + user: expressClerk("user"), + org: expressClerk("org"), + }, + ["supabase"]: { + user: expressSupabase("user"), + org: expressSupabase("org"), + }, + }, + ["elysia"]: { + ["better_auth"]: { + user: elysiaBetterAuth("user"), + org: elysiaBetterAuth("org"), + }, + }, + } as any; +}; + +export const AutumnHandler = () => { + const { queryStates } = useIntegrateContext(); + + const getSnippetContent = () => { + const backendLang = queryStates.backend; + const authProvider = queryStates.auth; + const customerType = queryStates.customerType; + + const templates = snippet(); + + console.log("Backend Lang", backendLang); + console.log("Auth Provider", authProvider); + + const template = + templates[backendLang]?.[authProvider]?.[customerType] || ""; + + console.log("Template", template); + + // Trim \n from the template (only left and right) + return template.trim("\n"); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx new file mode 100644 index 000000000..d364d7ce1 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx @@ -0,0 +1,44 @@ +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { StepHeader } from "../StepHeader"; + +const installCode = `npm install autumn-js`; +const installCodePnpm = `pnpm add autumn-js`; +const installCodeYarn = `yarn add autumn-js`; +const installCodeBun = `bun add autumn-js`; + +export const Install = () => { + return ( +
+ + + +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts b/vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts new file mode 100644 index 000000000..d5477ad2b --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/handlerSnippets.ts @@ -0,0 +1,74 @@ +export const nextjsBetterAuthUser = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.user.id, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +export const nextjsBetterAuthOrg = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.session.activeOrganizationId, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +export const nextjsBetterAuthOther = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + // From the request, retrieve the ID you want to use as the customer ID + const customerId = "customer_id"; + + return { + customerId, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx new file mode 100644 index 000000000..6c356fbba --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/betterAuth.tsx @@ -0,0 +1,33 @@ +export const betterAuthSnippet = ( + customerType: "user" | "org", + headersString: string, + tabLevel: number = 0 +) => { + const tabs = " ".repeat(tabLevel); + + let snippet = ``; + if (customerType === "user") { + snippet = `${tabs}const session = await auth.api.getSession({ +${tabs} headers: ${headersString}, +${tabs}}); +${tabs} +${tabs}return { +${tabs} customerId: session?.user.id, +${tabs} customerData: { +${tabs} name: session?.user.name, +${tabs} email: session?.user.email, +${tabs} }, +${tabs}};`; + } else { + snippet = `${tabs}const session = await auth.api.getSession({ +${tabs} headers: ${headersString}, +${tabs}}); +${tabs} +${tabs}return { +${tabs} customerId: session?.session.activeOrganizationId, +${tabs} customerData: { name: "", email: "" } +${tabs}};`; + } + + return snippet; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx new file mode 100644 index 000000000..8b1862693 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx @@ -0,0 +1,26 @@ +import { betterAuthSnippet } from "./betterAuth"; + +export const elysiaBetterAuth = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/elysia"; +import { auth } from "./auth"; + +const app = new Elysia({ adapter: node() }) + .use(cors()) + .mount(auth.handler) + .use( + autumnHandler({ + identify: async (context) => { +${betterAuthSnippet(customerType, "context.headers", 4)} + }, + }) + ) + .listen(8000); + + `; +}; + +// export const elysiaOther = (customerType: "user" | "org") => { +// return ` + +// `; +// }; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx new file mode 100644 index 000000000..b251636f2 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx @@ -0,0 +1,48 @@ +import { betterAuthSnippet } from "./betterAuth"; +import { clerkSnippet, supabaseSnippet } from "./honoHandler"; + +export const expressBetterAuth = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; +import { auth } from "@/lib/auth"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req) => { +${betterAuthSnippet(customerType, "req.headers", 3)} + }, + }) +);`; +}; + +export const expressClerk = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; +import { clerkMiddleware, getAuth } from "@clerk/express"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use(clerkMiddleware()); +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req) => { + ${clerkSnippet(customerType, "req")} + }, + }) +);`; +}; + +export const expressSupabase = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; +import { createClient } from "./lib/supabase"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req, res) => { + ${supabaseSnippet(customerType, "createClient({ req, res })")} + }, + }) +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx new file mode 100644 index 000000000..b323421a3 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx @@ -0,0 +1,106 @@ +import { betterAuthSnippet } from "./betterAuth"; + +export const clerkSnippet = ( + customerType: "user" | "org", + reqParam: string = "c" +) => { + if (customerType === "user") { + return `const auth = getAuth(${reqParam}); + + if (!auth?.userId) return null; + + return { + customerId: auth.userId, + customerData: { name: "", email: "" }, + }; + `; + } + + return `const auth = getAuth(${reqParam}); + + if (!auth?.userId || !auth?.orgId) return null; + + return { + customerId: auth.orgId, + customerData: { name: "", email: "" }, + }; + `; +}; + +export const honoClerk = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/hono"; +import { clerkMiddleware, getAuth } from "@hono/clerk-auth"; + +app.use("*", clerkMiddleware()); +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { + ${clerkSnippet(customerType)} + }, + }) +);`; +}; + +export const supabaseSnippet = ( + customerType: "user" | "org", + supabaseInit: string = "getSupabase(c)" +) => { + if (customerType === "user") { + return `const supabase = ${supabaseInit}; + + const { data, error } = await supabase.auth.getUser(); + + if (!data?.user?.id) return null; + + return { + customerId: data.user.id, + customerData: { name: "", email: "" }, + };`; + } + + return `const supabase = ${supabaseInit}; + + const { data, error } = await supabase.auth.getUser(); + + if (!data?.user?.id) return null; + + const orgId = "users_org_id"; // Get the orgId from your DB + + return { + customerId: orgId, + customerData: { name: "", email: "" }, + };`; +}; + +export const honoSupabase = (customerType: "user" | "org") => { + return `// index.ts + +import { autumnHandler } from "autumn-js/hono"; +import { getSupabase, supabaseMiddleware } from "./middleware/auth.middleware.js"; + +app.use("*", supabaseMiddleware()); +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { + ${supabaseSnippet(customerType)} + }, + }) +);`; +}; +export const honoBetterAuth = (customerType: "user" | "org") => { + return `// index.ts + +import { autumnHandler } from "autumn-js/hono"; +import { auth } from "@/lib/auth" + +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { +${betterAuthSnippet(customerType, "c.req.raw.headers", 3)} + }, + }) +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx new file mode 100644 index 000000000..035cf0348 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx @@ -0,0 +1,164 @@ +export const nextjsBetterAuthUser = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.user.id, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +export const nextjsBetterAuthOrg = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@/lib/auth"; +import { headers } from "next/headers"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + return { + customerId: session?.session.activeOrganizationId, + customerData: { + name: session?.user.name, + email: session?.user.email, + }, + }; + }, +}); +`; + +// Supabase Auth +export const nextjsSupabaseUser = ` +// app/api/autumn/[...all]/route.ts + +import { createClient } from "@/utils/supabase/server"; +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const supabase = await createClient(); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) { + return null; + } + + return { + customerId: data.user.id, + customerData: { + name: data.user.user_metadata?.name, + email: data.user.email, + }, + }; + }, +});`; + +export const nextjsSupabaseOrg = ` +// app/api/autumn/[...all]/route.ts + +import { createClient } from "@/utils/supabase/server"; +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const supabase = await createClient(); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) { + return null; + } + + // Get the orgId of the user from your DB + const customerId = "users_org_id"; + + return { + customerId, + customerData: { + name: data.user.user_metadata?.name, + email: data.user.email, + }, + }; + }, +}); +`; + +export const nextjsClerkUser = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@clerk/nextjs/server"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const { userId } = await auth(); + + if (!userId) return null; + + return { + customerId: userId, + // To store the customer name and email + customerData: { name: "", email: "" }, + }; + }, +}); +`; + +export const nextjsClerkOrg = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; +import { auth } from "@clerk/nextjs/server"; + +export const { GET, POST } = autumnHandler({ + identify: async () => { + const { userId, orgId } = await auth(); + + if (!userId || !orgId) return null; + + return { + customerId: orgId, + // To store the customer name and email + customerData: { name: "", email: "" }, + }; + }, +}); +`; + +export const nextjsOther = ` +// app/api/autumn/[...all]/route.ts + +import { autumnHandler } from "autumn-js/next"; + +export const { GET, POST } = autumnHandler({ + identify: async (request) => { + + // Authenticate the request and get the customer ID + const customerId = "customer_id"; + + return { + customerId, + // To store the customer name and email + customerData: { name: "", email: "" }, + }; + }, +}); +`; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx new file mode 100644 index 000000000..9cc41764c --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx @@ -0,0 +1,73 @@ +import { betterAuthSnippet } from "./betterAuth"; +import { supabaseAuthSnippet } from "./supabaseAuth"; + +export const rr7BetterAuth = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; +import { auth } from "@/lib/auth"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { +${betterAuthSnippet(customerType, "args.request.headers", 2)} + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; +export const rr7Supabase = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; +import { createClient } from "@/utils/supabase/server"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { + ${supabaseAuthSnippet({ customerType })} + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; + +export const clerkSnippet = (customerType: "user" | "org") => { + if (customerType === "user") { + return `const { userId } = await getAuth(args); + + if (!userId) return null; + + return { + customerId: userId, + customerData: { name: "", email: "" }, + };`; + } + + return `const { userId, orgId } = await getAuth(args); + + if (!userId || !orgId) return null; + + return { + customerId: orgId, + customerData: { name: "", email: "" }, + };`; +}; +export const rr7Clerk = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; +import { getAuth } from "@clerk/react-router/ssr.server"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { + ${clerkSnippet(customerType)} + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx new file mode 100644 index 000000000..9c7087120 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/supabaseAuth.tsx @@ -0,0 +1,35 @@ +export const supabaseAuthSnippet = ({ + backendLang, + customerType, +}: { + backendLang?: string; + customerType: "user" | "org"; +}) => { + if (customerType === "user") { + return `const supabase = await createClient(request); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) return null; + + return { + customerId: "123", + customerData: { + name: data.user.user_metadata?.name, + email: data.user.email, + }, + };`; + } + + return `const supabase = await createClient(request); + const { data, error } = await supabase.auth.getUser(); + + if (error || !data?.user) return null; + + // Get the orgId from your DB + const orgId = "users_org_id"; + + return { + customerId: orgId, + customerData: { name: "", email: "" }, + };`; +}; diff --git a/vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx b/vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx new file mode 100644 index 000000000..551dad797 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/AddTrialButton.tsx @@ -0,0 +1,64 @@ +import { Button } from "@/components/ui/button"; +import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeTrial"; +import { useState } from "react"; +import { useProductContext } from "@/views/products/product/ProductContext"; +import { PlusIcon, Trash, X } from "lucide-react"; +import { handleAutoSave } from "./model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; + +export const AddTrialButton = () => { + const { product, setProduct, mutate } = useProductContext(); + const [open, setOpen] = useState(false); + const axiosInstance = useAxiosInstance(); + + return ( + <> + + +
+ ) : ( +

+ Add Free Trial + +

+ )} + + + ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx new file mode 100644 index 000000000..d01b73d41 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx @@ -0,0 +1,215 @@ +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { ToggleButton } from "@/components/general/ToggleButton"; +import { Input } from "@/components/ui/input"; +import { slugify } from "@/utils/formatUtils/formatTextUtils"; +import { FeaturesContext } from "@/views/features/FeaturesContext"; +import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeTrial"; +import { CreateProductItem2 } from "@/views/products/product/product-item/CreateProductItem2"; +import { ProductItemTable } from "@/views/products/product/product-item/ProductItemTable"; +import { ProductContext } from "@/views/products/product/ProductContext"; +import { Button } from "@/components/ui/button"; +import { AddTrialButton } from "./AddTrialButton"; +import { useEffect, useState } from "react"; +import { useEnv } from "@/utils/envUtils"; +import { handleAutoSave } from "./model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useModelPricingContext } from "./ModelPricingContext"; +import { ProductRowToolbar } from "@/views/products/components/ProductRowToolbar"; + +export const EditProduct = ({ + data, + mutate, + product, + setProduct, +}: { + data: any; + mutate: any; + product: any; + setProduct: any; +}) => { + const [details, setDetails] = useState({ + name: product.name, + id: product.id, + }); + + const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); + const [entityFeatureIds, setEntityFeatureIds] = useState([]); + const [features, setFeatures] = useState([]); + const { productCounts, editingNewProduct, setEditingNewProduct } = + useModelPricingContext(); + const axiosInstance = useAxiosInstance(); + const env = useEnv(); + + useEffect(() => { + if (data) { + setFeatures(data.features); + } + }, [data]); + + const hasItems = product.items.length > 0; + + const handleToggleSettings = async (key: string) => { + const curValue = product[key]; + const newProduct = { ...product, [key]: !curValue }; + + setProduct(newProduct); + + handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { ...product, [key]: !curValue }, + mutate, + }); + }; + + return ( +
+
+ + +
+
+
+
+ + Name + + { + await handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { + ...product, + name: details.name, + id: details.id, + }, + mutate, + }); + setProduct({ + ...product, + name: details.name, + id: details.id, + }); + }} + placeholder="Free Plan" + value={details.name} + onChange={(e) => { + const curProduct = data?.products.find( + (p: any) => p.id === details.id + ); + console.log("Cur product:", curProduct); + const newIdData = editingNewProduct + ? { + id: slugify(e.target.value), + } + : {}; + setDetails({ + ...details, + name: e.target.value, + ...newIdData, + }); + }} + /> +
+
+ ID + +
+
+
+ +
+
+ +
+ +
+ +
+ +
+
+
+ handleToggleSettings("is_default")} + /> +
+ A default product is enabled by default for all new users, + typically used for your free plan. +
+
+
+ handleToggleSettings("is_add_on")} + /> +
+ A default product is enabled by default for all new users, + typically used for your free plan. +
+
+
+ +
+ Add a free trial to your product. +
+
+
+
+
+
+
+
+ ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/EditProductOld.tsx b/vite/src/views/onboarding2/model-pricing/EditProductOld.tsx new file mode 100644 index 000000000..2f088a441 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/EditProductOld.tsx @@ -0,0 +1,192 @@ +import { DialogFooter } from "@/components/ui/dialog"; +import { ProductService } from "@/services/products/ProductService"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useEnv } from "@/utils/envUtils"; +import { getBackendErr } from "@/utils/genUtils"; +import { FeaturesContext } from "@/views/features/FeaturesContext"; +import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeTrial"; +import { ManageProduct } from "@/views/products/product/ManageProduct"; +import { ProductContext } from "@/views/products/product/ProductContext"; +import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; +import { TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; +import { Check, X } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Tooltip } from "@/components/ui/tooltip"; +import { toast } from "sonner"; +import { ToggleButton } from "@/components/general/ToggleButton"; + +export const EditProductDialog = ({ + product, + features, + setProduct, + setFeatures, + mutate, + open, + setOpen, + originalProduct, + entityFeatureIds, + setEntityFeatureIds, +}: { + product: any; + setProduct: (product: any) => void; + features: any[]; + setFeatures: (features: any[]) => void; + mutate: () => Promise; + open: boolean; + setOpen: (open: boolean) => void; + originalProduct: any; + entityFeatureIds: string[]; + setEntityFeatureIds: (entityFeatureIds: string[]) => void; +}) => { + const env = useEnv(); + const axiosInstance = useAxiosInstance(); + const [createProductLoading, setCreateProductLoading] = useState(false); + const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); + + // Store the original product state when modal opens + const handleOpenChange = async (newOpen: boolean) => { + if (!newOpen && open && product?.id) { + // Modal is being closed, check if there are changes + const hasChanges = + originalProduct && + JSON.stringify(product) !== JSON.stringify(originalProduct); + + if (hasChanges) { + // Only update if there are changes + updateProduct(); + } + } + setOpen(newOpen); + }; + + const updateProduct = async () => { + setCreateProductLoading(true); + try { + const res = await ProductService.updateProduct( + axiosInstance, + product.id, + product + ); + toast.success("Product updated successfully"); + await mutate(); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to update product")); + } + setCreateProductLoading(false); + }; + + const handleFreeTrialClick = () => { + if (product?.free_trial) { + // Delete the free trial + setProduct({ ...product, free_trial: null }); + } else { + // Open the free trial modal + setFreeTrialModalOpen(true); + } + }; + + return ( + + + + {/* Edit Product */} + +
+ + + + + + +
+ +
+
+ + setProduct({ + ...product, + is_default: !product?.is_default, + }) + } + /> + + setProduct({ ...product, is_add_on: !product?.is_add_on }) + } + /> + +
+ ) : ( + //
+ + // {/* */} + //
+

Add Free Trial

+ )} + +
+ +
+ + + + ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx b/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx new file mode 100644 index 000000000..ae529b184 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx @@ -0,0 +1,259 @@ +import PricingTable from "@/components/autumn/pricing-table"; +import { EditProduct } from "./EditProduct"; +import { useEffect, useState } from "react"; +import { + getProductItemResponse, + ProductProperties, + ProductV2, + sortProductsV2, + UsageModel, +} from "@autumn/shared"; +import { getBackendErr, notNullish, nullish } from "@/utils/genUtils"; +import { isFreeProduct, isOneOffProduct } from "@/utils/product/priceUtils"; +import { Product } from "autumn-js"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { PlusIcon } from "lucide-react"; +import { Tabs, TabsTrigger, TabsList } from "@/components/ui/tabs"; +import { + ModelPricingContext, + useModelPricingContext, +} from "./ModelPricingContext"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Input } from "@/components/ui/input"; +import { slugify } from "@/utils/formatUtils/formatTextUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { toast } from "sonner"; +import { ProductsContext } from "@/views/products/ProductsContext"; +import { SelectEditProduct } from "./SelectEditProduct"; + +const defaultProduct = { + id: "", + name: "", + items: [], + is_default: false, + is_add_on: false, + free_trial: null, +}; + +export const ModelPricing = ({ + data, + mutate, + autumnProducts, + productCounts, +}: { + data: any; + mutate: any; + autumnProducts: Product[]; + productCounts: any; +}) => { + const curProduct = autumnProducts.length > 0 ? autumnProducts[0] : null; + + const [product, setProduct] = useState( + (curProduct && + data.products.find((p: ProductV2) => p.id === autumnProducts[0].id)) || + (defaultProduct as unknown as ProductV2) + ); + + const [firstItemCreated, setFirstItemCreated] = useState( + autumnProducts.some((p: Product) => p.items.length > 0) + ); + + const [editingNewProduct, setEditingNewProduct] = useState( + nullish(curProduct) + ); + + // Get latest product + const getAutumnProducts = () => { + const curProductItems = product.items.map((item: any) => + getProductItemResponse({ + item, + features: data.features, + currency: "USD", + }) + ); + + return autumnProducts; + // return [product, ...autumnProducts]; + + // const properties: ProductProperties = { + // has_trial: notNullish(product.free_trial), + // is_free: isFreeProduct(product.items), + // is_one_off: isOneOffProduct(product.items), + // updateable: product.items.some( + // (item: any) => item.usage_model == UsageModel.Prepaid + // ), + // }; + + // const latestProduct = { + // ...product, + // items: curProductItems, + // properties, + // }; + + // const curProducts = autumnProducts.filter( + // (p: Product) => p.id !== product.id + // ); + + // if (!firstItemCreated) { + // return []; + // } + + // const newProducts = [latestProduct, ...curProducts] as any; + // return sortProductsV2({ products: newProducts }) as Product[]; + }; + + useEffect(() => { + if (data) { + const curProduct = data.products.find( + (p: Product) => p.id === product.id + ); + + if (!curProduct) { + if (data.products.length > 0) { + setProduct(data.products[0]); + } + } + } + }, [data]); + + return ( + + +
+
+
+
+

Create your plans

+ {firstItemCreated && ( +
+ + +
+ )} +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+ ); +}; + +const NewProductPopover = () => { + const [open, setOpen] = useState(false); + const { mutate, data, setProduct } = useModelPricingContext(); + + const axiosInstance = useAxiosInstance(); + const [details, setDetails] = useState({ + name: "", + id: "", + }); + + const [creating, setCreating] = useState(false); + + const handleSave = async () => { + try { + setCreating(true); + await axiosInstance.post("/v1/products", { + name: details.name, + id: details.id, + }); + await mutate(); + const newProduct = { + ...defaultProduct, + name: details.name, + id: details.id, + }; + setProduct(newProduct); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to create product")); + } finally { + setCreating(false); + } + }; + + return ( + + + + + +
+

New Product

+
+ + setDetails({ + ...details, + name: e.target.value, + id: slugify(e.target.value), + }) + } + /> + +
+
+ +
+
+
+
+ ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx b/vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx new file mode 100644 index 000000000..a1ac58699 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/ModelPricingContext.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +export const ModelPricingContext = createContext(null); + +export const useModelPricingContext = () => { + const context = useContext(ModelPricingContext); + + if (context === undefined) { + throw new Error( + "useProductContext must be used within a ProductContextProvider" + ); + } + + return context; +}; diff --git a/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx b/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx new file mode 100644 index 000000000..176453160 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx @@ -0,0 +1,64 @@ +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useModelPricingContext } from "./ModelPricingContext"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; +import { ChevronDownIcon } from "lucide-react"; + +export const SelectEditProduct = () => { + const { data, product, setProduct } = useModelPricingContext(); + + if (data.products.length > 3) { + return ( + + + + + + {data.products.map((p: any) => { + if (!p.name) { + return null; + } + return ( + setProduct(p)}> + {p.name} + + ); + })} + + + ); + } + + const tabTriggerClass = + "data-[state=active]:bg-stone-200 data-[state=active]:text-t2 data-[state=active]:font-medium"; + + return ( + + + {data.products.map((p: any) => { + if (!p.name) { + return null; + } + return ( + setProduct(p)} + > + {p.name} + + ); + })} + + + ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts new file mode 100644 index 000000000..bbf3dd218 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils.ts @@ -0,0 +1,31 @@ +import { getBackendErr, notNullish } from "@/utils/genUtils"; +import { ProductV2 } from "@autumn/shared"; +import { AxiosInstance } from "axios"; +import { toast } from "sonner"; + +export const handleAutoSave = async ({ + axiosInstance, + productId, + product, + mutate, +}: { + axiosInstance: AxiosInstance; + productId: string; + product: ProductV2; + mutate: any; +}) => { + if (!productId || !product.id) return; + try { + await axiosInstance.post( + `v1/products/${productId}?upsert=true&disable_version=true`, + { + ...product, + group: notNullish(product.group) ? product.group : undefined, + } + ); + await mutate(); + } catch (error) { + console.log(error); + toast.error(getBackendErr(error, "Failed to auto save product")); + } +}; diff --git a/vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx new file mode 100644 index 000000000..587eae21f --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/model-pricing-utils/productToPricingCard.tsx @@ -0,0 +1,7 @@ +export const productToPricingCard = (product: any) => { + return { + id: product.id, + name: product.name, + items: product.items, + }; +}; diff --git a/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx b/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx new file mode 100644 index 000000000..60352f603 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx @@ -0,0 +1,17 @@ +import { useAxiosSWR } from "@/services/useAxiosSwr"; + +export const useListProducts = () => { + const { data, isLoading, error, mutate } = useAxiosSWR({ + url: "/v1/products", + options: { + refreshInterval: 0, + }, + }); + + return { + products: data?.list || [], + isLoading, + error, + mutate, + }; +}; diff --git a/vite/src/views/products/components/ProductRowToolbar.tsx b/vite/src/views/products/components/ProductRowToolbar.tsx index dc305d212..53475bd62 100644 --- a/vite/src/views/products/components/ProductRowToolbar.tsx +++ b/vite/src/views/products/components/ProductRowToolbar.tsx @@ -22,10 +22,12 @@ import { DeleteProductDialog } from "./DeleteProductDialog"; export const ProductRowToolbar = ({ product, productCounts, + isOnboarding = false, }: { className?: string; product: Product; productCounts: ProductCounts; + isOnboarding?: boolean; }) => { const [deleteLoading, setDeleteLoading] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -69,44 +71,48 @@ export const ProductRowToolbar = ({ - - { - e.stopPropagation(); - e.preventDefault(); - setSelectedProduct(product); - setDialogType("copy"); - setModalOpen(true); - }} - > -
- Copy - {copyLoading ? ( - - ) : ( - - )} -
-
-
- - { - e.stopPropagation(); - e.preventDefault(); - setSelectedProduct(product); - setDialogType("update"); - setModalOpen(true); - }} - > -
- Edit - -
-
-
+ {!isOnboarding && ( + + { + e.stopPropagation(); + e.preventDefault(); + setSelectedProduct(product); + setDialogType("copy"); + setModalOpen(true); + }} + > +
+ Copy + {copyLoading ? ( + + ) : ( + + )} +
+
+
+ )} + {!isOnboarding && ( + + { + e.stopPropagation(); + e.preventDefault(); + setSelectedProduct(product); + setDialogType("update"); + setModalOpen(true); + }} + > +
+ Edit + +
+
+
+ )} { diff --git a/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx b/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx index 52d9ad153..377e378a8 100644 --- a/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx +++ b/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx @@ -10,6 +10,8 @@ import { useProductContext } from "../ProductContext"; import { FreeTrialConfig } from "./FreeTrialConfig"; import { toast } from "sonner"; import { FreeTrialDuration } from "@autumn/shared"; +import { handleAutoSave } from "@/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const CreateFreeTrial = ({ open, @@ -21,7 +23,10 @@ export const CreateFreeTrial = ({ // const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [price, setPrice] = useState(null); - const { env, product, setProduct, prices } = useProductContext(); + const { env, product, setProduct, prices, autoSave, mutate } = + useProductContext(); + + const axiosInstance = useAxiosInstance(); const [freeTrial, setFreeTrial] = useState({ length: 7, @@ -44,6 +49,15 @@ export const CreateFreeTrial = ({ duration: freeTrial.duration, }, }); + + if (autoSave) { + handleAutoSave({ + axiosInstance, + productId: product.id, + product: { ...product, free_trial: freeTrial }, + mutate, + }); + } setOpen(false); }; diff --git a/vite/src/views/products/product/product-item/CreateProductItem.tsx b/vite/src/views/products/product/product-item/CreateProductItem.tsx index fdbd6618b..3965bc517 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem.tsx @@ -11,19 +11,17 @@ import { useState } from "react"; import { ProductItemConfig } from "./ProductItemConfig"; import { ProductItemContext } from "./ProductItemContext"; import { CreateFeature } from "@/views/features/CreateFeature"; + import { ProductItemInterval, ProductItem, CreateFeature as CreateFeatureType, - UpdateProductSchema, } from "@autumn/shared"; import { useProductContext } from "../ProductContext"; import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; - import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; import { ItemConfigFooter } from "./product-item-config/item-config-footer/ItemConfigFooter"; -import { ProductService } from "@/services/products/ProductService"; import { useAxiosInstance } from "@/services/useAxiosInstance"; export const defaultProductItem: ProductItem = { @@ -173,7 +171,7 @@ export function CreateProductItem() { (features.length == 0 && item.price === null) ? (
) : ( - + {}} /> )} diff --git a/vite/src/views/products/product/product-item/CreateProductItem2.tsx b/vite/src/views/products/product/product-item/CreateProductItem2.tsx new file mode 100644 index 000000000..be9850657 --- /dev/null +++ b/vite/src/views/products/product/product-item/CreateProductItem2.tsx @@ -0,0 +1,114 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +import { useEffect, useState } from "react"; +import { ProductItemConfig } from "./ProductItemConfig"; +import { ProductItemContext } from "./ProductItemContext"; +import { CreateFeature } from "@/views/features/CreateFeature"; + +import { + ProductItemInterval, + ProductItem, + CreateFeature as CreateFeatureType, +} from "@autumn/shared"; + +import { useProductContext } from "../ProductContext"; +import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; +import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; +import { ItemConfigFooter } from "./product-item-config/item-config-footer/ItemConfigFooter"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { PlusIcon } from "lucide-react"; +import { CreateItemDialogContent } from "./create-product-item/CreateItemDialogContent"; +import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelPricingContext"; + +const defaultProductItem: ProductItem = { + feature_id: null, + + included_usage: null, + + interval: ProductItemInterval.Month, + + // Price config + price: null, + tiers: null, + billing_units: 1, + + // Others + entity_feature_id: null, + reset_usage_when_enabled: true, +}; + +export function CreateProductItem2() { + const [open, setOpen] = useState(false); + const [showCreateFeature, setShowCreateFeature] = useState(false); + const [item, setItem] = useState(defaultProductItem); + const { features, product, setProduct, setFeatures } = useProductContext(); + const { firstItemCreated, setFirstItemCreated } = useModelPricingContext(); + + const handleCreateProductItem = async (entityFeatureId?: string) => { + const validatedItem = validateProductItem({ + item: { + ...item, + entity_feature_id: entityFeatureId + ? entityFeatureId + : item.entity_feature_id, + }, + features, + }); + + if (!validatedItem) return; + + const newItems = [...product.items, validatedItem]; + const newProduct = { ...product, items: newItems }; + setProduct(newProduct); + setTimeout(() => { + setItem({ + ...defaultProductItem, + feature_id: null, + }); + }, 400); + + setOpen(false); + setFirstItemCreated(true); + return newProduct; + }; + + return ( + + +
+ + + +
+ +
+
+ ); +} diff --git a/vite/src/views/products/product/product-item/ProductItemConfig.tsx b/vite/src/views/products/product/product-item/ProductItemConfig.tsx index 27b4d8ceb..daa7c74a5 100644 --- a/vite/src/views/products/product/product-item/ProductItemConfig.tsx +++ b/vite/src/views/products/product/product-item/ProductItemConfig.tsx @@ -22,9 +22,7 @@ import { isFeaturePriceItem, isPriceItem } from "@/utils/product/getItemType"; export const ProductItemConfig = () => { // HOOKS const { features } = useProductContext(); - const { item, setItem } = useProductItemContext(); - const [show, setShow] = useState(getShowParams(item)); const handleAddPrice = () => { diff --git a/vite/src/views/products/product/product-item/ProductItemRow.tsx b/vite/src/views/products/product/product-item/ProductItemRow.tsx index 75e5139ea..619b1ba38 100644 --- a/vite/src/views/products/product/product-item/ProductItemRow.tsx +++ b/vite/src/views/products/product/product-item/ProductItemRow.tsx @@ -21,6 +21,7 @@ import { Flag } from "lucide-react"; import { cn } from "@/lib/utils"; import { isFeatureItem, isPriceItem } from "@/utils/product/getItemType"; import { notNullish } from "@/utils/genUtils"; +import { useProductContext } from "../ProductContext"; interface ProductItemRowProps { item: ProductItem; @@ -29,6 +30,7 @@ interface ProductItemRowProps { features: Feature[]; org: any; onRowClick: (item: ProductItem, index: number) => void; + className?: string; } export const ProductItemRow = ({ @@ -38,7 +40,9 @@ export const ProductItemRow = ({ features, org, onRowClick, + className, }: ProductItemRowProps) => { + const { product } = useProductContext(); const getName = ({ featureId, units, @@ -200,13 +204,15 @@ export const ProductItemRow = ({ }; const itemType = getItemType(item); + const isLast = index === product.items.length - 1; return (
onRowClick(item, index)} > @@ -226,7 +232,12 @@ export const ProductItemRow = ({ : getPaidFeatureString(item)} - + { - const { product, setProduct, features, org, entityFeatureIds } = +export const ProductItemTable = () => { + const { product, features, org, entityFeatureIds, isOnboarding } = useProductContext(); const [selectedItem, setSelectedItem] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null); const [open, setOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false); - const [entitiesOpen, setEntitiesOpen] = useState(false); const [freeTrialOpen, setFreeTrialOpen] = useState(false); const handleRowClick = (item: ProductItem, index: number) => { @@ -44,11 +39,11 @@ export const ProductItemTable = ({ const groupedItems = entityFeatureIds.reduce( (acc: Record, entityFeatureId: string) => { acc[entityFeatureId] = product.items.filter( - (item: ProductItem) => item.entity_feature_id === entityFeatureId, + (item: ProductItem) => item.entity_feature_id === entityFeatureId ); return acc; }, - {} as Record, + {} as Record ); return ( @@ -64,65 +59,81 @@ export const ProductItemTable = ({

Product Items

+
-
- - - -
+ {/*
*/} + {!isOnboarding && } + + +
*/}
+ + {/* */}
-
+ +
{/* Original product items mapping - excluding items that appear in grouped sections */} {product.items .filter( (item: ProductItem) => !entityFeatureIds.some( (entityFeatureId: string) => - item.entity_feature_id === entityFeatureId, - ), + item.entity_feature_id === entityFeatureId + ) ) .map((item: ProductItem, index: number) => ( -

+

{entityFeatureId}

@@ -163,7 +179,7 @@ export const ProductItemTable = ({ org={org} onRowClick={handleRowClick} /> - ), + ) )} {/* Show message if no items for this entityFeatureId */} @@ -171,7 +187,7 @@ export const ProductItemTable = ({
Add the features this entity gets access to @@ -181,8 +197,17 @@ export const ProductItemTable = ({ ))} {product.items.length === 0 && ( -
+

+ Product items determine what customers get access to and how + they're billed. Start by adding one. +

+ {/*

Product items determine what customers get access to and how they're billed{" "} Priced Features:{" "} features that have a price based on usage (eg, $1 per credit)

-
+
*/}
)}
diff --git a/vite/src/views/products/product/product-item/UpdateProductItem.tsx b/vite/src/views/products/product/product-item/UpdateProductItem.tsx index 5a13461b0..584d7f86c 100644 --- a/vite/src/views/products/product/product-item/UpdateProductItem.tsx +++ b/vite/src/views/products/product/product-item/UpdateProductItem.tsx @@ -28,18 +28,19 @@ export default function UpdateProductItem({ const [showCreateFeature, setShowCreateFeature] = useState(false); const handleUpdateProductItem = () => { - console.log("Selected Item: ", selectedItem); const validatedItem = validateProductItem({ item: selectedItem!, features, }); - if (!validatedItem) return; + if (!validatedItem) return null; if (notNullish(selectedIndex)) { const newProduct = { ...product }; newProduct.items[selectedIndex!] = validatedItem; setProduct(newProduct); setOpen(false); + + return newProduct; } }; @@ -49,6 +50,7 @@ export default function UpdateProductItem({ newProduct.items.splice(selectedIndex!, 1); setProduct(newProduct); setOpen(false); + return newProduct; } }; diff --git a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx index b9c43880d..b5dead984 100644 --- a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx +++ b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx @@ -8,10 +8,11 @@ import { import { useProductItemContext } from "../ProductItemContext"; import { useProductContext } from "../../ProductContext"; import { FeatureTypeBadge } from "@/views/features/FeatureTypeBadge"; -import { Feature } from "@autumn/shared"; +import { Feature, FeatureType, ProductItemType } from "@autumn/shared"; import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { PlusIcon } from "lucide-react"; +import { getItemType } from "@/utils/product/productItemUtils"; export const SelectItemFeature = ({ show, @@ -24,6 +25,8 @@ export const SelectItemFeature = ({ const { item, setItem, setShowCreateFeature, isUpdate } = useProductItemContext(); + const itemType = getItemType(item); + return (
- setQueryStates({ ...queryStates, frontend: value }) - } +
+ +

+ Help us customize the integration guide for your specific tech stack. + Click{" "} + { + setQueryStates({ ...queryStates, reactTypescript: false }); + }} + className="underline cursor-pointer" > - - - - - {frontendOptions.map((option) => ( - - {option.label} - - ))} - - -

- - {/* Backend Framework */} -
- - -
- - {/* Auth Provider */} -
- - -
- - {/* Customer Type */} -
- - -
+ here + {" "} + if you're not using a React + Typescript backend stack. +

- - {/* {(stack.frontend || - stack.backend || - stack.auth || - stack.customerType) && ( -
-

Selected Stack:

-
- {stack.frontend && ( -

- Frontend:{" "} - {frontendOptions.find((f) => f.value === stack.frontend)?.label} -

- )} - {stack.backend && ( -

- Backend:{" "} - {backendOptions.find((b) => b.value === stack.backend)?.label} -

- )} - {stack.auth && ( -

- Auth:{" "} - {authOptions.find((a) => a.value === stack.auth)?.label} -

- )} - {stack.customerType && ( -

- Customers:{" "} - { - customerOptions.find((c) => c.value === stack.customerType) - ?.label - } -

- )} -
-
- )} */} + {queryStates.reactTypescript ? ( + + ) : ( + <> + +

+ This onboarding guide shows how to set up Autumn using our + frontend components / hooks on React and server-side framework + adaptors. +
+
+ In your case, you should integrate with Autumn's API directly on + the backend. We have SDKs for Typescript and Python. Learn how to + do so{" "} +
+ here + + . +

+ + + + )}
); }; diff --git a/vite/src/views/onboarding2/integrate/StackEnums.tsx b/vite/src/views/onboarding2/integrate/StackEnums.tsx new file mode 100644 index 000000000..243a5317b --- /dev/null +++ b/vite/src/views/onboarding2/integrate/StackEnums.tsx @@ -0,0 +1,15 @@ +export enum Backend { + Nextjs = "nextjs", + ReactRouter = "react-router", + Express = "express", + Elysia = "elysia", + Hono = "hono", + Other = "other", +} + +export enum Frontend { + Nextjs = "nextjs", + ReactRouter = "react-router", + Vite = "vite", + Other = "other", +} diff --git a/vite/src/views/onboarding2/integrate/StepHeader.tsx b/vite/src/views/onboarding2/integrate/StepHeader.tsx index 8ac04206d..79751e60b 100644 --- a/vite/src/views/onboarding2/integrate/StepHeader.tsx +++ b/vite/src/views/onboarding2/integrate/StepHeader.tsx @@ -3,14 +3,14 @@ export const StepHeader = ({ title, }: { number: number; - title: string; + title: React.ReactNode; }) => { return (
{number}
-

{title}

+
{title}
); }; diff --git a/vite/src/views/onboarding2/integrate/components/CodeSpan.tsx b/vite/src/views/onboarding2/integrate/components/CodeSpan.tsx new file mode 100644 index 000000000..ffbc6ef17 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/components/CodeSpan.tsx @@ -0,0 +1,7 @@ +export const CodeSpan = ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ); +}; diff --git a/vite/src/views/onboarding2/integrate/components/InfoBox.tsx b/vite/src/views/onboarding2/integrate/components/InfoBox.tsx new file mode 100644 index 000000000..6462880a6 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/components/InfoBox.tsx @@ -0,0 +1,27 @@ +import { cn } from "@/lib/utils"; +import { Info } from "lucide-react"; + +export const InfoBox = ({ + classNames, + children, +}: { + classNames?: { + infoIcon?: string; + infoBox?: string; + }; + children: React.ReactNode; +}) => { + return ( +
+
+ +
+ {children} +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx new file mode 100644 index 000000000..8026fdbad --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx @@ -0,0 +1,162 @@ +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { CodeSpan } from "../components/CodeSpan"; +import { StepHeader } from "../StepHeader"; +import { useIntegrateContext } from "../IntegrateContext"; +import { Backend, Frontend } from "../StackEnums"; +import { InfoBox } from "../components/InfoBox"; + +const nextjsAutumnProvider = ({ + includeBackendUrl, +}: { + includeBackendUrl: boolean; +}) => { + const backendUrlStr = includeBackendUrl + ? ` backendUrl={process.env.NEXT_PUBLIC_BACKEND_URL}` + : ""; + return `// layout.tsx + +import { AutumnProvider, PricingTable } from "autumn-js/react"; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +}`; +}; + +const rr7AutumnProvider = ({ + includeBackendUrl, +}: { + includeBackendUrl: boolean; +}) => { + const backendUrlStr = includeBackendUrl + ? ` backendUrl={import.meta.env.VITE_BACKEND_URL}` + : ""; + return `// root.tsx + +import { AutumnProvider } from "autumn-js/react"; +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +}`; +}; + +const viteAutumnProvider = () => { + return `// main.tsx +import { AutumnProvider } from "autumn-js/react"; + +createRoot(document.getElementById("root")!).render( + + + +);`; +}; + +const otherAutumnProvider = () => { + return `// main.tsx +import { AutumnProvider } from "autumn-js/react"; + +// 1. Simply wrap the root of your app in the AutumnProvider component +// 2. Pass in your server's URL to the backendUrl prop + +createRoot(document.getElementById("root")!).render( + + + + + +);`; +}; + +const getSnippet = (queryStates: any) => { + const { backend, frontend } = queryStates; + if (frontend === Frontend.Nextjs) { + return nextjsAutumnProvider({ + includeBackendUrl: backend !== Backend.Nextjs, + }); + } else if (frontend === Frontend.ReactRouter) { + return rr7AutumnProvider({ + includeBackendUrl: backend === Backend.Nextjs, + }); + } else if (frontend === Frontend.Vite) { + return viteAutumnProvider(); + } else { + return otherAutumnProvider(); + } +}; + +export const AddAutumnProvider = () => { + const { queryStates } = useIntegrateContext(); + return ( +
+ + Wrap your React app in {""} +

+ } + /> +

+ This allows you to use our React hooks and components in your app. If + your server URL is different to your client, you will need to pass in + the backend URL as a prop. +

+ + {queryStates.auth === "supabase" && ( + +

+ The examples above assume that Supabase auth is implemented using + server-side cookie authentication, following the guide{" "} + + here + + . +
+
+ If you need to set {"Bearer "} in + your request headers to authenticate, you can use the{" "} + getBearerToken prop in the{" "} + AutumnProvider component. +

+
+ )} +
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx index 6346ba3fb..a3b34292b 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx @@ -12,22 +12,36 @@ import { nextjsSupabaseOrg, nextjsSupabaseUser, } from "./snippets/nextjsHandler"; -import { rr7BetterAuth, rr7Clerk, rr7Supabase } from "./snippets/rr7Handler"; +import { + rr7BetterAuth, + rr7Clerk, + rr7Other, + rr7Supabase, +} from "./snippets/rr7Handler"; import { honoBetterAuth, honoClerk, honoSupabase, + honoOther, } from "./snippets/honoHandler"; import { expressBetterAuth, expressClerk, + expressOther, expressSupabase, } from "./snippets/expressHandler"; -import { elysiaBetterAuth } from "./snippets/elysiaHandler"; +import { + elysiaBetterAuth, + elysiaClerk, + elysiaOther, +} from "./snippets/elysiaHandler"; +import { general } from "./snippets/general"; +import { CodeSpan } from "../components/CodeSpan"; +import { Backend } from "../StackEnums"; const snippet = () => { return { - nextjs: { + [Backend.Nextjs]: { ["better_auth"]: { user: nextjsBetterAuthUser, org: nextjsBetterAuthOrg, @@ -45,7 +59,7 @@ const snippet = () => { org: nextjsOther, }, }, - ["react_router"]: { + [Backend.ReactRouter]: { ["better_auth"]: { user: rr7BetterAuth("user"), org: rr7BetterAuth("org"), @@ -58,8 +72,12 @@ const snippet = () => { user: rr7Clerk("user"), org: rr7Clerk("org"), }, + ["other"]: { + user: rr7Other("user"), + org: rr7Other("org"), + }, }, - ["hono"]: { + [Backend.Hono]: { ["clerk"]: { user: honoClerk("user"), org: honoClerk("org"), @@ -72,8 +90,12 @@ const snippet = () => { user: honoBetterAuth("user"), org: honoBetterAuth("org"), }, + ["other"]: { + user: honoOther("user"), + org: honoOther("org"), + }, }, - ["express"]: { + [Backend.Express]: { ["better_auth"]: { user: expressBetterAuth("user"), org: expressBetterAuth("org"), @@ -86,12 +108,24 @@ const snippet = () => { user: expressSupabase("user"), org: expressSupabase("org"), }, + ["other"]: { + user: expressOther("user"), + org: expressOther("org"), + }, }, - ["elysia"]: { + [Backend.Elysia]: { ["better_auth"]: { user: elysiaBetterAuth("user"), org: elysiaBetterAuth("org"), }, + ["clerk"]: { + user: elysiaClerk("user"), + org: elysiaClerk("org"), + }, + ["other"]: { + user: elysiaOther("user"), + org: elysiaOther("org"), + }, }, } as any; }; @@ -106,21 +140,25 @@ export const AutumnHandler = () => { const templates = snippet(); - console.log("Backend Lang", backendLang); - console.log("Auth Provider", authProvider); + let template = templates[backendLang]?.[authProvider]?.[customerType] || ""; - const template = - templates[backendLang]?.[authProvider]?.[customerType] || ""; + template = template.trim("\n"); - console.log("Template", template); + if (!template) { + return general(); + } - // Trim \n from the template (only left and right) - return template.trim("\n"); + return template; }; return ( -
- +
+ +

+ autumnHandler mounts routes on the{" "} + /api/autumn/* paths which allows our React hooks + and components to interact with the Autumn API directly. +

{ + return `import { PricingTable } from "autumn-js/react"; + +export default function Home() { + return ( +
+
+ +
+
+ ); +}`; +}; + +const getSnippet = (queryStates: any) => { + return nextjsSnippet(); +}; + +export const CheckoutPricingTable = () => { + const { queryStates } = useIntegrateContext(); + return ( +
+ + Drop in {""} +

+ } + /> +

+ Display a pricing table with the plans you have created and let your + customers choose a plan. +

+ + +

+ { + "Our component is completely customisable by installing it as a shadcn component. " + } + Learn how to do so{" "} + + here + + . +

+
+
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/CreateSecretKey.tsx b/vite/src/views/onboarding2/integrate/integration-steps/CreateSecretKey.tsx new file mode 100644 index 000000000..dc22a6e12 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/CreateSecretKey.tsx @@ -0,0 +1,114 @@ +import { Input } from "@/components/ui/input"; + +import { DevContext } from "@/views/developer/DevContext"; + +import Step from "@/components/general/OnboardingStep"; + +import { useEnv } from "@/utils/envUtils"; +import { useState } from "react"; +import { toast } from "sonner"; +import { DevService } from "@/services/DevService"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { Button } from "@/components/ui/button"; +import { CheckIcon, CopyIcon, PlusIcon } from "lucide-react"; + +export const CreateSecretKey = ({ + apiKey, + setApiKey, +}: { + apiKey: string; + setApiKey: (apiKey: string) => void; +}) => { + const env = useEnv(); + // const [apiKeyName, setApiKeyName] = useState(""); + const [apiCreated, setApiCreated] = useState(false); + + const [loading, setLoading] = useState(false); + const [copied, setCopied] = useState(false); + const axiosInstance = useAxiosInstance({ env }); + + const handleCreate = async () => { + setLoading(true); + try { + const { api_key } = await DevService.createAPIKey(axiosInstance, { + name: "Autumn Onboarding", + }); + + setApiKey(api_key); + } catch (error) { + console.log("Error:", error); + toast.error("Failed to create API key"); + } + + setLoading(false); + }; + + return ( + {}, + onboarding: true, + apiCreated, + setApiCreated, + }} + > +
+ {apiKey ? ( +
+ + +
+ ) : ( +
+ {/* setApiKeyName(e.target.value)} + /> */} + +
+ )} +
+
+ ); +}; + +{ + /*
+ {env === AppEnv.Sandbox ? ( + + ) : ( + + )} +
*/ +} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx b/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx new file mode 100644 index 000000000..28e682ca8 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx @@ -0,0 +1,36 @@ +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { CodeSpan } from "../components/CodeSpan"; +import { StepHeader } from "../StepHeader"; +import { useState } from "react"; +import { CreateSecretKey } from "./CreateSecretKey"; + +export const EnvStep = () => { + const [apiKey, setApiKey] = useState(""); + return ( + <> +
+ + Add the Autumn secret key to your {".env"}{" "} + file +

+ } + /> + + + +
+ + ); +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx index 8b1862693..9c99bb277 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/elysiaHandler.tsx @@ -4,7 +4,7 @@ export const elysiaBetterAuth = (customerType: "user" | "org") => { return `import { autumnHandler } from "autumn-js/elysia"; import { auth } from "./auth"; -const app = new Elysia({ adapter: node() }) +const app = new Elysia() .use(cors()) .mount(auth.handler) .use( @@ -19,8 +19,60 @@ ${betterAuthSnippet(customerType, "context.headers", 4)} `; }; -// export const elysiaOther = (customerType: "user" | "org") => { -// return ` +export const elysiaClerk = (customerType: "user" | "org") => { + return `import { clerkPlugin } from "elysia-clerk"; +import { autumnHandler } from "autumn-js/backend"; -// `; -// }; +const app = new Elysia() + .use(cors()) + .use(clerkPlugin()) + .all("*", async (ctx: any) => { + console.log("Request received"); + }) + .all("/api/autumn/*", async (ctx: any) => { + const auth = ctx.auth(); + + let body = null; + if (ctx.request.method !== "GET") { + body = await ctx.request.json(); + } + + const { statusCode, response } = await autumnHandler({ + customerId: ${customerType === "user" ? "auth.userId" : "auth.orgId"}, + customerData: { name: "", email: "" }, + request: { + url: ctx.request.url, + method: ctx.request.method, + body: body, + }, + }); + + ctx.set.status = statusCode; + return response; + }) + .listen(8000);`; +}; + +export const elysiaOther = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/elysia"; +import { auth } from "./auth"; + +const app = new Elysia() + .use(cors()) + .mount(auth.handler) + .use( + autumnHandler({ + identify: async (context) => { + const customerId = "your_customer_id"; // Authenticate and get customer ID from your DB + + return { + customerId, + customerData: { name: "", email: "" }, + }; + }, + }) + ) + .listen(8000); + + `; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx index b251636f2..10c666549 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/expressHandler.tsx @@ -46,3 +46,22 @@ app.use( }) );`; }; + +export const expressOther = (customerType: "user" | "org") => { + return `import { autumnHandler } from "autumn-js/express"; + +app.use(express.json()); // need to parse request body before autumnHandler +app.use( + "/api/autumn", + autumnHandler({ + identify: async (req, res) => { + const customerId = "your_customer_id"; // Get customer id from your database + + return { + customerId, + customerData: { name: "", email: "" }, + }; + }, + }) +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/general.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/general.tsx new file mode 100644 index 000000000..e3da5a42c --- /dev/null +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/general.tsx @@ -0,0 +1,41 @@ +export const general = () => { + return `import { autumnHandler } from "autumn-js/backend"; + +// 1. autumnHandler takes in request properties and returns a response +// 2. Simply mount the handler onto the /api/autumn/* path in your backend +// 3. Call autumnHandler and pass in the required parameters +// 4. Return the response from the autumnHandler + +// Example using autumnHandler with Hono & Clerk +import { autumnHandler } from "autumn-js/backend"; +import { clerkMiddleware, getAuth } from "@hono/clerk-auth"; + +app.use("*", clerkMiddleware()); +app.use( + "/api/autumn/*", + async (c) => { + const auth = getAuth(c); + + if (!auth?.userId) { + return c.json({ message: "Unauthorized" }, 401); + } + + let body = null; + if (c.req.method !== "GET") { + body = await c.req.json(); + } + + const { statusCode, response } = await autumnHandler({ + customerId: auth.userId, + customerData: { name: "", email: "" }, + request: { + url: c.req.url, + method: c.req.method, + body: body, + }, + }); + + return c.json(response, statusCode); + } +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx index b323421a3..f052f0815 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/honoHandler.tsx @@ -104,3 +104,23 @@ ${betterAuthSnippet(customerType, "c.req.raw.headers", 3)} }) );`; }; +export const honoOther = (customerType: "user" | "org") => { + return `// index.ts + +import { autumnHandler } from "autumn-js/hono"; +import { auth } from "@/lib/auth" + +app.use( + "/api/autumn/*", + autumnHandler({ + identify: async (c: Context) => { + const customerId = "your_customer_id"; // Get customer id from your database + + return { + customerId, + customerData: { name: "", email: "" }, + }; + }, + }) +);`; +}; diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx index 035cf0348..b295fd420 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/nextjsHandler.tsx @@ -50,8 +50,8 @@ export const { GET, POST } = autumnHandler({ export const nextjsSupabaseUser = ` // app/api/autumn/[...all]/route.ts -import { createClient } from "@/utils/supabase/server"; import { autumnHandler } from "autumn-js/next"; +import { createClient } from "@/utils/supabase/server"; export const { GET, POST } = autumnHandler({ identify: async () => { @@ -75,8 +75,8 @@ export const { GET, POST } = autumnHandler({ export const nextjsSupabaseOrg = ` // app/api/autumn/[...all]/route.ts -import { createClient } from "@/utils/supabase/server"; import { autumnHandler } from "autumn-js/next"; +import { createClient } from "@/utils/supabase/server"; export const { GET, POST } = autumnHandler({ identify: async () => { @@ -150,13 +150,10 @@ import { autumnHandler } from "autumn-js/next"; export const { GET, POST } = autumnHandler({ identify: async (request) => { - - // Authenticate the request and get the customer ID - const customerId = "customer_id"; - + // Authenticate the request and get the customer ID + const customerId = "customer_id"; return { customerId, - // To store the customer name and email customerData: { name: "", email: "" }, }; }, diff --git a/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx index 9cc41764c..6c4a3e15c 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/snippets/rr7Handler.tsx @@ -71,3 +71,23 @@ const handler = autumnHandler({ export const loader = handler.loader; export const action = handler.action;`; }; +export const rr7Other = (customerType: "user" | "org") => { + return `// app/routes/api.autumn.tsx + +import { autumnHandler } from "autumn-js/react-router"; + +const handler = autumnHandler({ + secretKey: process.env.AUTUMN_SECRET_KEY!, + identify: async (args) => { + const customerId = "your_customer_id"; // Get customer id from your database + + return { + customerId, + customerData: { name: "", email: "" }, + }; + }, +}); + +export const loader = handler.loader; +export const action = handler.action;`; +}; diff --git a/vite/src/views/onboarding2/integrate/select-stack/SelectFrameworks.tsx b/vite/src/views/onboarding2/integrate/select-stack/SelectFrameworks.tsx new file mode 100644 index 000000000..5fe996607 --- /dev/null +++ b/vite/src/views/onboarding2/integrate/select-stack/SelectFrameworks.tsx @@ -0,0 +1,172 @@ +import { Code, Fingerprint, CircleUserRound, Building } from "lucide-react"; +import { useIntegrateContext } from "../IntegrateContext"; +import { Backend, Frontend } from "../StackEnums"; + +export const SelectFrameworks = () => { + const { queryStates, setQueryStates } = useIntegrateContext(); + + const iconSize = 12; + const frontendOptions = [ + { value: Frontend.Nextjs, label: "Next.js", logo: "nextjs.png" }, + { value: Frontend.ReactRouter, label: "RR7", logo: "react-router.svg" }, + { value: Frontend.Vite, label: "Vite SPA", logo: "vite.svg" }, + { + value: Frontend.Other, + label: "Other", + icon: , + }, + ]; + + const backendOptions = [ + { value: Backend.Nextjs, label: "Next.js", logo: "nextjs.png" }, + { + value: Backend.ReactRouter, + label: "RR7", + logo: "react-router.svg", + }, + { value: Backend.Hono, label: "Hono", logo: "hono.png" }, + { value: Backend.Express, label: "Express", logo: "express.png" }, + { value: Backend.Elysia, label: "Elysia", logo: "elysia.png" }, + { + value: Backend.Other, + label: "Other", + icon: , + }, + ]; + + const authOptions = [ + { value: "better_auth", label: "Better Auth", logo: "better-auth.png" }, + { value: "supabase", label: "Supabase", logo: "supabase.png" }, + { value: "clerk", label: "Clerk", logo: "clerk.png" }, + { + value: "other", + label: "Other", + icon: , + }, + ]; + + const customerOptions = [ + { + value: "user", + label: "Users", + icon: , + }, + { + value: "org", + label: "Orgs", + icon: , + }, + ]; + return ( +
+
+ +
+ {frontendOptions.map((option) => { + return ( + + ); + })} +
+
+
+ +
+ {backendOptions.map((option) => { + return ( + + ); + })} +
+
+ +
+
+ +
+ {authOptions.map((option) => { + return ( + + ); + })} +
+
+
+ +
+ {customerOptions.map((option) => { + return ( + + ); + })} +
+
+
+
+ ); +}; + +const FrameworkContainer = ({ + option, + setQueryStates, + queryStates, + type, +}: { + option: { + value: string; + label: string; + logo?: string; + icon?: React.ReactNode; + }; + setQueryStates: (queryStates: any) => void; + queryStates: any; + type: "backend" | "auth" | "customerType" | "frontend"; +}) => { + const isSelected = queryStates[type] === option.value; + return ( +
setQueryStates({ ...queryStates, [type]: option.value })} + > + {option.logo ? ( + {option.label} + ) : option.icon ? ( + <>{option.icon} + ) : ( + + )} + + {option.label} + +
+ ); +}; From abc076657575fcdd46483352cdc1d9a5ecf1f26b Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 3 Aug 2025 19:54:19 -0700 Subject: [PATCH 08/37] fix: analytics page section header styling --- vite/src/components/general/PageSectionHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite/src/components/general/PageSectionHeader.tsx b/vite/src/components/general/PageSectionHeader.tsx index 2288f9a5d..1f73a23c4 100644 --- a/vite/src/components/general/PageSectionHeader.tsx +++ b/vite/src/components/general/PageSectionHeader.tsx @@ -38,8 +38,8 @@ export const PageSectionHeader = ({ )} {titleComponent}
- {endContent}
+ {endContent} {addButton &&
{addButton}
} {menuComponent && (
{menuComponent}
From dbb79332cc35daf30311fe90c6632f451c6da396 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 4 Aug 2025 20:54:59 -0700 Subject: [PATCH 09/37] finishing onboarding --- bun.lock | 86 ++-- server/package.json | 2 +- server/src/db/initDrizzle.ts | 4 - .../external/stripe/stripeInvoiceSubUtils.ts | 4 +- .../attach/checkout/handleCheckout.ts | 1 - server/src/internal/features/featureUtils.ts | 12 +- .../features/handlers/handleCreateFeature.ts | 4 +- .../features/internalFeatureRouter.ts | 113 ++-- server/src/internal/mainRouter.ts | 2 +- .../products/internalProductRouter.ts | 10 +- server/src/queue/workersInit.ts | 74 +-- server/src/utils/errorUtils.ts | 28 +- vite/package.json | 2 +- .../src/components/autumn/checkout-dialog.tsx | 30 +- vite/src/components/autumn/pricing-table.tsx | 29 +- .../modal-components/DialogContentWrapper.tsx | 56 +- vite/src/components/ui/button.tsx | 1 + vite/src/utils/formatUtils/formatUtils.ts | 27 + vite/src/views/credits/CreateCreditSystem.tsx | 36 +- vite/src/views/credits/CreditSystemConfig.tsx | 24 +- vite/src/views/credits/UpdateCreditSystem.tsx | 29 +- vite/src/views/features/CreateFeature.tsx | 145 +++--- vite/src/views/features/UpdateFeature.tsx | 36 ++ .../components/CreateFeatureFooter.tsx | 41 ++ .../features/hooks/useFeatureDialogState.tsx | 21 + .../metered-features/FeatureConfig.tsx | 2 +- .../views/features/utils/defaultFeature.ts | 21 + .../src/views/onboarding2/OnboardingView2.tsx | 179 +++---- vite/src/views/onboarding2/SampleApp.tsx | 487 ++++++++++++++++++ .../views/onboarding2/integrate/AITools.tsx | 99 ++-- .../onboarding2/integrate/IntegrateAutumn.tsx | 59 ++- .../views/onboarding2/integrate/NextSteps.tsx | 29 ++ .../onboarding2/integrate/SelectStack.tsx | 4 +- .../model-pricing/AddTrialButton.tsx | 18 +- .../model-pricing/ConnectStripe.tsx | 82 +++ .../onboarding2/model-pricing/EditProduct.tsx | 186 +++---- .../model-pricing/ModelPricing.tsx | 207 +++++--- .../model-pricing/SelectEditProduct.tsx | 144 ++++-- .../edit-product/EditProductDetails.tsx | 75 +++ .../model-pricing/usePricingTable.tsx | 4 +- .../onboarding2/utils/useCustomerReplica.tsx | 37 ++ .../views/products/product/ManageProduct.tsx | 2 +- .../views/products/product/ProductView.tsx | 27 +- .../products/product/hooks/useProductData.tsx | 24 +- .../product/prices/CreateFixedPrice.tsx | 4 +- .../product-item/CreateProductItem.tsx | 59 ++- .../product-item/CreateProductItem2.tsx | 27 +- .../product/product-item/EntitiesDropdown.tsx | 26 +- .../product/product-item/ProductItemTable.tsx | 12 +- .../product-item/UpdateProductItem.tsx | 26 +- .../components/ConfigWithFeature.tsx | 4 +- .../components/SelectItemFeature.tsx | 17 +- .../CreateFeatureFromItem.tsx | 42 +- .../CreateItemDialogContent.tsx | 145 +++--- .../create-product-item/CreateItemIntro.tsx | 100 +++- .../advanced-config/AdvancedItemConfig.tsx | 3 +- .../components/IncludedUsage.tsx | 2 +- .../components/SelectFeature.tsx | 76 +++ .../item-config-footer/ItemConfigFooter.tsx | 21 +- .../product/product-item/useSteps.tsx | 56 ++ .../product-item/utils/CreateItemStep.tsx | 6 + .../products/product/utils/updateProduct.ts | 32 ++ 62 files changed, 2230 insertions(+), 931 deletions(-) create mode 100644 vite/src/utils/formatUtils/formatUtils.ts create mode 100644 vite/src/views/features/components/CreateFeatureFooter.tsx create mode 100644 vite/src/views/features/hooks/useFeatureDialogState.tsx create mode 100644 vite/src/views/features/utils/defaultFeature.ts create mode 100644 vite/src/views/onboarding2/SampleApp.tsx create mode 100644 vite/src/views/onboarding2/integrate/NextSteps.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx create mode 100644 vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx create mode 100644 vite/src/views/onboarding2/utils/useCustomerReplica.tsx create mode 100644 vite/src/views/products/product/product-item/product-item-config/components/SelectFeature.tsx create mode 100644 vite/src/views/products/product/product-item/useSteps.tsx create mode 100644 vite/src/views/products/product/product-item/utils/CreateItemStep.tsx create mode 100644 vite/src/views/products/product/utils/updateProduct.ts diff --git a/bun.lock b/bun.lock index d4672561a..2ad733296 100644 --- a/bun.lock +++ b/bun.lock @@ -47,7 +47,7 @@ "@supabase/supabase-js": "^2.46.2", "@upstash/redis": "^1.35.1", "ai": "^4.3.10", - "autumn-js": "^0.0.77", + "autumn-js": "^0.1.4", "axios": "^1.8.3", "better-auth": "^1.2.9", "body-parser": "^1.20.3", @@ -167,7 +167,7 @@ "ag-charts-community": "^12.0.2", "ag-grid-community": "^34.0.2", "ag-grid-react": "^34.0.2", - "autumn-js": "^0.1.0", + "autumn-js": "^0.1.4", "axios": "^1.8.3", "better-auth": "^1.2.9", "class-variance-authority": "^0.7.1", @@ -961,13 +961,13 @@ "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], - "@sentry/core": ["@sentry/core@9.44.0", "", {}, "sha512-U+KBNGgq/eXIj226CPtRk+n5dx0q1xGVvbLbyfAyeek9C/wxQ3f+mvqeVqF9cx8FfrWIOeDM1F8ISH5uRkjjQg=="], + "@sentry/core": ["@sentry/core@9.44.2", "", {}, "sha512-4wduCY9vz+VRMZXTT1dzk08L2nReeR+lzpY8hCcc+Wu100BoJR+TNlrSn1rG5iIo98NDW860JsRA7SVDUDOiNQ=="], - "@sentry/node": ["@sentry/node@9.44.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", "@sentry/core": "9.44.0", "@sentry/node-core": "9.44.0", "@sentry/opentelemetry": "9.44.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-rU96Q7q7hL4s328z9zFS+ZRK6eHnLFjYbH8XHCxAxGFDLyg9kpkR5to9PjoI+QVPZ/LYAE+Xw0wStoMjWMCFsA=="], + "@sentry/node": ["@sentry/node@9.44.2", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", "@sentry/core": "9.44.2", "@sentry/node-core": "9.44.2", "@sentry/opentelemetry": "9.44.2", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-HTUDD73Tdr4GvvcNGQunkqEKeijHb4WYq/NX4YZP5VOeOsKsgIUsv55EgWk1BSHAFGTW6bfeMSoqaNVWiRHn0w=="], - "@sentry/node-core": ["@sentry/node-core@9.44.0", "", { "dependencies": { "@sentry/core": "9.44.0", "@sentry/opentelemetry": "9.44.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-M6HOcA73WWzRuhqw4Fd2dqv9zEsvMteSNYOguTexIQCT2pzk1srACrt4uFfLY01s9FIKjw+tjrQfTbni2adv7Q=="], + "@sentry/node-core": ["@sentry/node-core@9.44.2", "", { "dependencies": { "@sentry/core": "9.44.2", "@sentry/opentelemetry": "9.44.2", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-TnyKZQ4FOCA+mkLLaOzFPePUBRBf0FU62hnNMscJviwb0UloOvHXx4Ub1DudfFFdnIeVSSMU96ou8vW1zR/1Uw=="], - "@sentry/opentelemetry": ["@sentry/opentelemetry@9.44.0", "", { "dependencies": { "@sentry/core": "9.44.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-OeMiVoLqEXtpYE2VBAGmhK4GfbUa5ivDtL+AF4B+cR+NZkqZFlnA7ItquVfAa2Jd45TIyueEK8yjan5hluQYJQ=="], + "@sentry/opentelemetry": ["@sentry/opentelemetry@9.44.2", "", { "dependencies": { "@sentry/core": "9.44.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-KeW5MPXyq9Q8ieYUHO0PuzNNYEYizmTH6x02PG400GwmoeNxnT59Afa4TuPcrXN0QUmK76HJuPfC+7CTuCgoKA=="], "@sentry/types": ["@sentry/types@8.55.0", "", { "dependencies": { "@sentry/core": "8.55.0" } }, "sha512-6LRT0+r6NWQ+RtllrUW2yQfodST0cJnkOmdpHA75vONgBUhpKwiJ4H7AmgfoTET8w29pU6AnntaGOe0LJbOmog=="], @@ -1129,7 +1129,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + "@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -1181,41 +1181,41 @@ "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20250802.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250802.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250802.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20250802.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250802.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20250802.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250802.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20250802.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CuxxalPlgXIiAN2b884D/3UX6M1FvAi+MgruRVp4kuKOfpmBjJr6PRhN12iekFQwRG2fPil8svIm4mPnSM71lw=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20250804.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250804.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250804.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20250804.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250804.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20250804.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250804.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20250804.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-EVYSeheDmdLrvwWRW4RXl47rUMEst6JkO7hgvwYEF8TBQ4zrBWcMlCulhgI5MsvCrNf7hkVn6628bCkp/5Ik2A=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20250802.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b8qDaguy/41W2PsA4TmGZ8wlaAg/SzG3jGJTPxv5o3tsyLQ7J/KqHljTox7RPC4tOnTzlsHYJXIuOUM+ZEIGcw=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20250804.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Bn7ptzfXtXUmriod5fadXo5+j8VfeJmePm54IcbtqwxhyDkcvyHPhOhp2l7IMtpVOwuXTua/hid6HCHe0LXTaQ=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20250802.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-WnHvj1RcW5PeQ7vPcUhGV4F1iyl749EjiWc1FIji1nFK46AJ0NXEO6I+AeqkNTyhpnenDj1YRlqW0JyziiBZSg=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20250804.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-WN4sCqnEfrvr7n+YfZR/Wme5FZ88ASY5p2Zcvbm1bMO1FCn2h7iPnInoMfynsoYmfcWQhZqEaSkmK6HGeJKuWQ=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20250802.1", "", { "os": "linux", "cpu": "arm" }, "sha512-oEtMLt/iqADX7Dy4eVPQXzEA3kGVw47MEosHPg9CTnb9bZrEabwL8yC9knux12pqIH6zhdZAYkQIWxL6jUMOTA=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20250804.1", "", { "os": "linux", "cpu": "arm" }, "sha512-emtoOuf+K8TK9JnQn8RsNs/mNiPQ/u5xR+N1S7IQFgiEBrngPzCKSwDrtPbY9xGh9mVg7aGa58+Y/hAtXudFwQ=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20250802.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpDnhJTsbdOihfBCbnWfF6terDfgNtW/+rwFK1VbLY6JzOQdxWHiTwP+vYA1XBmNmQTidYGNF5ahfCYwx2kftw=="], + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20250804.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-N5NHuiT9gBDl3+sW0OGJPeSZYAwYZGfMf2ES4ZDj1Pd4TDR2b6S47FckQVrGWzVDImoqZZsJan5gs3cofTE8nA=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20250802.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z+QPLdLcjkFPQ+HbBunq6U50Rd6Ywmu5ICAY4NaKgu5l1u/AyyMMhVaSINWjlZacB8CHGRl7ONb1z/vtH/iVyA=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20250804.1", "", { "os": "linux", "cpu": "x64" }, "sha512-yT9JKfbuSh3hNybNqUHIsuoOvVsLka7JfRfNh2Hes1eJGVH04I4E5tKAlzI9Iy7OEWCovHZFblQRjp/3CJYxyA=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20250802.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-diHuHgkeqKvQeP9VHMMBv7KJ0RvnPQ+ROenosRiw+sC5wButa5gR+vl1CLOtuhZXawnuRu20cxx9OFOgoODTLA=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20250804.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-da5LkGm/l1zzpaTc2FwQh1F3H1pYSAUpqHzskcPL//SDTfrQnkbN9dPn+CI8cHDRv8lCAQFXII+xFFLWPMNXyQ=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20250802.1", "", { "os": "win32", "cpu": "x64" }, "sha512-tGTxLuIrEZG8+Pze1QWDoB6AcnoiQ6S1txaeGub0SD8nOCgAiNmqAmaJekeEYlmHm8cj8j0u2G/l+geC9StYDA=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20250804.1", "", { "os": "win32", "cpu": "x64" }, "sha512-zC+Wzi8Lr+RGF69PHHnesyoImBTwkzdPD9aoHTrynrP8Fcv+oRAUQmQcI0JFB0cEzFxMQ1sUMdBrAikdveaz1g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -1293,7 +1293,7 @@ "attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="], - "autumn-js": ["autumn-js@0.0.77", "", { "dependencies": { "chalk": "^5.4.1", "rou3": "^0.6.1", "swr": "^2.3.3" } }, "sha512-P5btZQjaq/y4A7lcd4aK3QgZBtLZKwwQypCvJrX8SmLG/Oe+wyuWzIhdNsa/fO5ATExNmAkdP344dSUlfbgQnQ=="], + "autumn-js": ["autumn-js@0.1.4", "", { "dependencies": { "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^3.24.1" }, "peerDependencies": { "better-auth": "^1.2.12", "better-call": "^1.0.12" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-sI7rw43x7DbmXPhy1TVK2hwFpOxW4fXH8xcn4f9kcxrD3Spxz2Y7JgG5D7Ef4ZkdpZGl7f2TT/PhftCYoWpYow=="], "axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="], @@ -1367,7 +1367,7 @@ "chai-http": ["chai-http@5.1.2", "", { "dependencies": { "@types/superagent": "^8.1.7", "charset": "^1.0.1", "cookiejar": "^2.1.4", "is-ip": "^5.0.1", "methods": "^1.1.2", "qs": "^6.12.1", "superagent": "^10.0.0" } }, "sha512-UFup7mUGkkjmi9bGA7F6vfp3lzGQZjtL//CEd+a4C+vlynSv756XHDUK8PoYk/UpTBBXqSghjQaJOUMUxJXNaA=="], - "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], + "chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="], "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], @@ -1447,7 +1447,7 @@ "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], - "core-js": ["core-js@3.44.0", "", {}, "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw=="], + "core-js": ["core-js@3.45.0", "", {}, "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA=="], "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], @@ -1575,7 +1575,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.194", "", {}, "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.195", "", {}, "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -2323,7 +2323,7 @@ "recaseai": ["recaseai@0.0.37", "", { "dependencies": { "@anthropic-ai/sdk": "^0.32.1", "@supabase/supabase-js": "^2.47.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.10.1", "async-listen": "^3.0.1", "axios": "^1.7.9", "commander": "^12.1.0", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "figures": "^6.1.0", "inquirer": "^12.1.0", "ksuid": "^3.0.0", "nanoid": "^5.0.9", "openai": "^4.76.0", "ora": "^8.1.1", "picocolors": "^1.1.1", "pino": "^9.5.0", "tsx": "^4.19.2", "typescript": "^5.7.2" }, "bin": { "recase": "dist/cli.js" } }, "sha512-cKVMWTGBnGtm8K+uD2vfMXOzxdHj1U3++vvTePpOoZABI/SY/jjDpW997vM5xeDnzz7Vk/qqVkLQUQ/ZMDXpsQ=="], - "recharts": ["recharts@3.1.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NqAqQcGBmLrfDs2mHX/bz8jJCQtG2FeXfE0GqpZmIuXIjkpIwj8sd9ad0WyvKiBKPd8ZgNG0hL85c8sFDwascw=="], + "recharts": ["recharts@3.1.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-PVA2gdAiTPaPj+56BV5qVfkuPxhqXBhWKnu7r+7WYsczCWHkSrZcE224GLOAWjUMj+cYTkYfGYV2WC2qdJtvcQ=="], "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], @@ -2341,7 +2341,7 @@ "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "resend": ["resend@4.7.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-30IbXGBUbmDweQH2IlO53XOXX7ndjYV9xFZ8IEBiWqefqQ/qmTsgrX0Ab6MUnmobJXbpdReVv+iXGRQPubQL5Q=="], + "resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="], "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], @@ -2431,7 +2431,7 @@ "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - "sonner": ["sonner@2.0.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -2567,13 +2567,13 @@ "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - "typescript-eslint": ["typescript-eslint@8.38.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/parser": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg=="], + "typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="], "uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="], "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], @@ -2693,8 +2693,6 @@ "@autumn/vite/@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="], - "@autumn/vite/autumn-js": ["autumn-js@0.1.0", "", { "dependencies": { "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^3.24.1" }, "peerDependencies": { "better-auth": "^1.2.12", "better-call": "^1.0.12" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-7YLnc33xYUhWEP0yCuEwWliZ+EOqvIz6ZTc4GvVKYNQwC2UIXX5USsWKf9maKfxQF1zSEMRcCmCa3hBLRcNUZg=="], - "@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], "@autumn/vite/stripe": ["stripe@18.4.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-LKFeDnDYo4U/YzNgx2Lc9PT9XgKN0JNF1iQwZxgkS4lOw5NunWCnzyH5RhTlD3clIZnf54h7nyMWkS8VXPmtTQ=="], @@ -3039,20 +3037,6 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/eslint-plugin/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - - "@typescript-eslint/parser/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - - "@typescript-eslint/project-service/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - - "@typescript-eslint/tsconfig-utils/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - - "@typescript-eslint/type-utils/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - - "@typescript-eslint/typescript-estree/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - - "@typescript-eslint/utils/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -3179,8 +3163,6 @@ "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "typescript-eslint/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - "winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/server/package.json b/server/package.json index 987c47413..f2e23949d 100644 --- a/server/package.json +++ b/server/package.json @@ -48,7 +48,7 @@ "@supabase/supabase-js": "^2.46.2", "@upstash/redis": "^1.35.1", "ai": "^4.3.10", - "autumn-js": "^0.0.77", + "autumn-js": "^0.1.4", "axios": "^1.8.3", "better-auth": "^1.2.9", "body-parser": "^1.20.3", diff --git a/server/src/db/initDrizzle.ts b/server/src/db/initDrizzle.ts index 240738a0e..3a8b4dea8 100644 --- a/server/src/db/initDrizzle.ts +++ b/server/src/db/initDrizzle.ts @@ -8,9 +8,6 @@ import { schemas as schema } from "@autumn/shared"; export let client = postgres(process.env.DATABASE_URL!); export let db = drizzle(client, { schema }); -// export const localClient = postgres("postgresql://postgres:postgres@localhost:54322/postgres") -// export const localDb = drizzle(localClient, { schema }); - export const initDrizzle = (params?: { maxConnections?: number }) => { let maxConnections = params?.maxConnections || 10; const client = postgres(process.env.DATABASE_URL!, { @@ -19,7 +16,6 @@ export const initDrizzle = (params?: { maxConnections?: number }) => { const db = drizzle(client, { schema, - // logger: process.env.NODE_ENV === "development", }); return { db, client }; diff --git a/server/src/external/stripe/stripeInvoiceSubUtils.ts b/server/src/external/stripe/stripeInvoiceSubUtils.ts index a1e5b2d42..24abc26a8 100644 --- a/server/src/external/stripe/stripeInvoiceSubUtils.ts +++ b/server/src/external/stripe/stripeInvoiceSubUtils.ts @@ -46,11 +46,11 @@ export const createStripeSubThroughInvoice = async ({ let subItems = items.filter( (i: any, index: number) => - prices[index].config!.interval !== BillingInterval.OneOff, + prices[index].config!.interval !== BillingInterval.OneOff ); let invoiceItems = items.filter( (i: any, index: number) => - prices[index].config!.interval === BillingInterval.OneOff, + prices[index].config!.interval === BillingInterval.OneOff ); try { diff --git a/server/src/internal/customers/attach/checkout/handleCheckout.ts b/server/src/internal/customers/attach/checkout/handleCheckout.ts index a8f6da458..b6b8fd6af 100644 --- a/server/src/internal/customers/attach/checkout/handleCheckout.ts +++ b/server/src/internal/customers/attach/checkout/handleCheckout.ts @@ -108,7 +108,6 @@ export const handleCheckout = (req: any, res: any) => handler: async (req: ExtendedRequest, res: ExtendedResponse) => { const { logger, features } = req; const attachBody = AttachBodySchema.parse(req.body); - // Pre-populate options... const { attachParams, flags, branch, config, func } = await getAttachVars( { req, attachBody } diff --git a/server/src/internal/features/featureUtils.ts b/server/src/internal/features/featureUtils.ts index ede7e65f1..b65a36bae 100644 --- a/server/src/internal/features/featureUtils.ts +++ b/server/src/internal/features/featureUtils.ts @@ -77,9 +77,9 @@ export const validateCreditSystem = (config: CreditSystemConfig) => { // Check if multiple of the same feature const meteredFeatureIds = schema.map( - (schemaItem) => schemaItem.metered_feature_id, + (schemaItem) => schemaItem.metered_feature_id ); - console.log("Metered feature ids:", meteredFeatureIds); + // console.log("Metered feature ids:", meteredFeatureIds); const uniqueMeteredFeatureIds = Array.from(new Set(meteredFeatureIds)); if (meteredFeatureIds.length !== uniqueMeteredFeatureIds.length) { throw new RecaseError({ @@ -135,14 +135,14 @@ export const getObjectsUsingFeature = async ({ }); let entitlements = allEnts.filter( - (entitlement) => entitlement.internal_feature_id == feature.internal_id, + (entitlement) => entitlement.internal_feature_id == feature.internal_id ); let linkedEntitlements = allEnts.filter( - (entitlement) => entitlement.entity_feature_id == feature.id, + (entitlement) => entitlement.entity_feature_id == feature.id ); let prices = allPrices.filter( - (price) => (price.config as any).internal_feature_id == feature.internal_id, + (price) => (price.config as any).internal_feature_id == feature.internal_id ); return { entitlements, prices, creditSystems, linkedEntitlements }; @@ -163,7 +163,7 @@ export const runSaveFeatureDisplayTask = async ({ try { if (!process.env.ANTHROPIC_API_KEY) { logger.warn( - "ANTHROPIC_API_KEY is not set, skipping feature display generation", + "ANTHROPIC_API_KEY is not set, skipping feature display generation" ); return; } diff --git a/server/src/internal/features/handlers/handleCreateFeature.ts b/server/src/internal/features/handlers/handleCreateFeature.ts index dc200a4d1..1e6344b80 100644 --- a/server/src/internal/features/handlers/handleCreateFeature.ts +++ b/server/src/internal/features/handlers/handleCreateFeature.ts @@ -9,11 +9,13 @@ import { handleFrontendReqError } from "@/utils/errorUtils.js"; export const handleCreateFeature = async (req: any, res: any) => { try { + console.log("Trying to create feature"); const data = req.body; let { db, orgId, env, logtail: logger } = req; let parsedFeature = validateFeature(data); - let feature: Feature = { + const feature: Feature = { + archived: false, internal_id: generateId("fe"), org_id: orgId, created_at: Date.now(), diff --git a/server/src/internal/features/internalFeatureRouter.ts b/server/src/internal/features/internalFeatureRouter.ts index fb2c3eb2c..51ec9964a 100644 --- a/server/src/internal/features/internalFeatureRouter.ts +++ b/server/src/internal/features/internalFeatureRouter.ts @@ -29,14 +29,14 @@ export const internalFeatureRouter: Router = express.Router(); internalFeatureRouter.get("", async (req: any, res: any) => { try { let { showArchived } = req.query; - + if (showArchived !== undefined) { // If showArchived is specified, use FeatureService.list with the parameter let features = await FeatureService.list({ db: req.db, orgId: req.orgId, env: req.env, - showOnlyArchived: showArchived === 'true', + showOnlyArchived: showArchived === "true", }); res.status(200).json({ features }); } else { @@ -50,8 +50,6 @@ internalFeatureRouter.get("", async (req: any, res: any) => { } }); - - export const validateFeature = (data: any) => { let featureType = data.type; @@ -96,26 +94,23 @@ export const initNewFeature = ({ internalFeatureRouter.post("", async (req: any, res) => { let data = req.body; - try { let { db, orgId, env, logtail: logger } = req; let parsedFeature = validateFeature(data); - - let feature: Feature = { + const feature: Feature = { + archived: false, internal_id: generateId("fe"), org_id: orgId, created_at: Date.now(), env: env, ...parsedFeature, }; - let org = await OrgService.getFromReq(req); let insertedData = await FeatureService.insert({ db, data: feature, logger, }); - await addTaskToQueue({ jobName: JobName.GenerateFeatureDisplay, payload: { @@ -123,7 +118,6 @@ internalFeatureRouter.post("", async (req: any, res) => { org: org, }, }); - let insertedFeature = insertedData && insertedData.length > 0 ? insertedData[0] : null; res.status(200).json(insertedFeature); @@ -132,60 +126,63 @@ internalFeatureRouter.post("", async (req: any, res) => { } }); -internalFeatureRouter.get("/data/deletion_text/:feature_id", async (req: any, res) => { - try { - let { db } = req; - let { feature_id } = req.params; - - // Get the feature first - let feature = await FeatureService.get({ - db, - id: feature_id, - orgId: req.orgId, - env: req.env, - }); +internalFeatureRouter.get( + "/data/deletion_text/:feature_id", + async (req: any, res) => { + try { + let { db } = req; + let { feature_id } = req.params; - if (!feature) { - return res.status(404).json({ error: "Feature not found" }); - } + // Get the feature first + let feature = await FeatureService.get({ + db, + id: feature_id, + orgId: req.orgId, + env: req.env, + }); - // Use Drizzle query similar to ProductService.getDeletionText - let res_data = await db - .select({ - productName: sql`CASE WHEN ROW_NUMBER() OVER (ORDER BY ${products.created_at}) = 1 THEN ${products.name ?? "Product name not found"} ELSE NULL END`, - totalCount: sql`COUNT(*) OVER ()`, - }) - .from(products) - .innerJoin( - entitlements, - eq(products.internal_id, entitlements.internal_product_id) - ) - .where( - and( - eq(entitlements.internal_feature_id, feature.internal_id!), - eq(products.env, req.env), - eq(products.org_id, req.orgId) + if (!feature) { + return res.status(404).json({ error: "Feature not found" }); + } + + // Use Drizzle query similar to ProductService.getDeletionText + let res_data = await db + .select({ + productName: sql`CASE WHEN ROW_NUMBER() OVER (ORDER BY ${products.created_at}) = 1 THEN ${products.name ?? "Product name not found"} ELSE NULL END`, + totalCount: sql`COUNT(*) OVER ()`, + }) + .from(products) + .innerJoin( + entitlements, + eq(products.internal_id, entitlements.internal_product_id) ) - ) - .limit(1); + .where( + and( + eq(entitlements.internal_feature_id, feature.internal_id!), + eq(products.env, req.env), + eq(products.org_id, req.orgId) + ) + ) + .limit(1); - // If no products found, return explicit zero count - if (!res_data || res_data.length === 0) { - res.status(200).json({ - productName: null, - totalCount: 0, - }); - } else { - res.status(200).json({ - productName: res_data[0]?.productName || null, - totalCount: Number(res_data[0]?.totalCount) || 0, - }); + // If no products found, return explicit zero count + if (!res_data || res_data.length === 0) { + res.status(200).json({ + productName: null, + totalCount: 0, + }); + } else { + res.status(200).json({ + productName: res_data[0]?.productName || null, + totalCount: Number(res_data[0]?.totalCount) || 0, + }); + } + } catch (error) { + console.error("Failed to get feature deletion text", error); + res.status(500).send(error); } - } catch (error) { - console.error("Failed to get feature deletion text", error); - res.status(500).send(error); } -}); +); internalFeatureRouter.post("/:feature_id", handleUpdateFeature); internalFeatureRouter.delete("/:featureId", handleDeleteFeature); diff --git a/server/src/internal/mainRouter.ts b/server/src/internal/mainRouter.ts index f7f80aee7..506d7eaf8 100644 --- a/server/src/internal/mainRouter.ts +++ b/server/src/internal/mainRouter.ts @@ -116,7 +116,7 @@ mainRouter.use( }, identify: async (req: any) => { return { - customerId: "user_123", + customerId: "onboarding_demo_user", customerData: { name: "Demo User", email: "demo@useautumn.com", diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index 7f34bea14..7e78e1e8c 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -33,7 +33,6 @@ productRouter.get("/data", async (req: any, res) => { db, orgId: req.orgId, env: req.env, - returnAll: true, archived: false, }), FeatureService.getFromReq(req), @@ -116,8 +115,17 @@ productRouter.get("/counts", async (req: any, res) => { // returnAll: true, }); + const latestVersion = req.query.latest_version === "true"; + let counts = await Promise.all( products.map(async (product) => { + if (latestVersion) { + return CusProdReadService.getCounts({ + db, + internalProductId: product.internal_id, + }); + } + return CusProdReadService.getCountsForAllVersions({ db, productId: product.id, diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index 80cc2b670..fbbe03249 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -49,42 +49,50 @@ const initWorker = ({ }, }); - if (job.name == JobName.DetectBaseVariant) { - await detectBaseVariant({ - db, - curProduct: job.data.curProduct, - logger: logtail as Logger, - }); - return; - } + try { + if (job.name == JobName.DetectBaseVariant) { + await detectBaseVariant({ + db, + curProduct: job.data.curProduct, + logger: logtail as Logger, + }); + return; + } - if (job.name == JobName.GenerateFeatureDisplay) { - await runSaveFeatureDisplayTask({ - db, - feature: job.data.feature, - logger: logtail, - }); - return; - } + if (job.name == JobName.GenerateFeatureDisplay) { + await runSaveFeatureDisplayTask({ + db, + feature: job.data.feature, + logger: logtail, + }); + return; + } - if (job.name == JobName.Migration) { - await runMigrationTask({ - db, - payload: job.data, - logger: logtail, - }); - return; - } + if (job.name == JobName.Migration) { + await runMigrationTask({ + db, + payload: job.data, + logger: logtail, + }); + return; + } - if (actionHandlers.includes(job.name as JobName)) { - await runActionHandlerTask({ - queue, - job, - logger: logtail, - db, - useBackup, + if (actionHandlers.includes(job.name as JobName)) { + await runActionHandlerTask({ + queue, + job, + logger: logtail, + db, + useBackup, + }); + return; + } + } catch (error) { + logtail.error(`Failed to process bullmq job: ${job.name}`, { + jobName: job.name, + jobData: job.data, + error, }); - return; } // TRIGGER CHECKOUT REWARD @@ -180,8 +188,6 @@ const initWorker = ({ console.log("JOB ID:", jobId); }); - // Check jobs left in queue - worker.on("error", async (error: any) => { if (error.code !== "ECONNREFUSED") { console.log("WORKER ERROR:", error.message); diff --git a/server/src/utils/errorUtils.ts b/server/src/utils/errorUtils.ts index cfb35edfc..1177f8877 100644 --- a/server/src/utils/errorUtils.ts +++ b/server/src/utils/errorUtils.ts @@ -191,15 +191,13 @@ export const handleFrontendReqError = ({ action: string; }) => { try { - let logger = req.logtail; - + const logger = req.logger; if ( error instanceof RecaseError && error.statusCode == StatusCodes.NOT_FOUND ) { - req.logtail.warn( - `(frontend) ${req.method} ${req.originalUrl}: not found` - ); + // Temporarily disable logger to prevent thread-stream crashes + console.log(`(frontend) ${req.method} ${req.originalUrl}: not found`); res.status(404).json({ message: error.message, code: error.code, @@ -207,21 +205,13 @@ export const handleFrontendReqError = ({ return; } - logger.warn(`FRONTEND REQUEST WARNING: ${error.message}`, { - type: "frontend_request", - error: { - message: error.message, - stack: error.stack, - code: error.code, - action, - }, - }); + logger.error( + `(frontend) ${req.method} ${req.originalUrl}: ${error.message}`, + { + error, + } + ); - // logger.warn(`${req.method} ${req.originalUrl}`, { - // type: "frontend_request", - // }); - // logger.warn(`${action}`); - // logger.warn(error); res.status(400).json({ message: error.message || "Unknown error", code: error.code || "unknown_error", diff --git a/vite/package.json b/vite/package.json index f9efc0753..435ef7d5d 100644 --- a/vite/package.json +++ b/vite/package.json @@ -41,7 +41,7 @@ "ag-charts-community": "^12.0.2", "ag-grid-community": "^34.0.2", "ag-grid-react": "^34.0.2", - "autumn-js": "^0.1.0", + "autumn-js": "^0.1.4", "axios": "^1.8.3", "better-auth": "^1.2.9", "class-variance-authority": "^0.7.1", diff --git a/vite/src/components/autumn/checkout-dialog.tsx b/vite/src/components/autumn/checkout-dialog.tsx index ff0589667..4452fcf7a 100644 --- a/vite/src/components/autumn/checkout-dialog.tsx +++ b/vite/src/components/autumn/checkout-dialog.tsx @@ -24,6 +24,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; +import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelPricingContext"; export interface CheckoutDialogProps { open: boolean; @@ -50,6 +51,8 @@ export default function CheckoutDialog(params: CheckoutDialogProps) { CheckoutResult | undefined >(params?.checkoutResult); + const { mutateAutumnProducts } = useModelPricingContext(); + useEffect(() => { if (params.checkoutResult) { setCheckoutResult(params.checkoutResult); @@ -70,11 +73,9 @@ export default function CheckoutDialog(params: CheckoutDialogProps) { return ( - + {title} -
- {message} -
+
{message}
{isPaid && checkoutResult && ( )} - + @@ -162,10 +162,10 @@ function DueAmounts({ checkoutResult }: { checkoutResult: CheckoutResult }) {
-

Total due today

+

Total due today

-

+

{formatCurrency({ amount: checkoutResult?.total, currency: checkoutResult?.currency, @@ -175,9 +175,9 @@ function DueAmounts({ checkoutResult }: { checkoutResult: CheckoutResult }) { {showNextCycle && (

-

Due next cycle ({nextCycleAtStr})

+

Due next cycle ({nextCycleAtStr})

-

+

{formatCurrency({ amount: next_cycle.total, currency: checkoutResult?.currency, @@ -242,9 +242,7 @@ function CheckoutLines({ checkoutResult }: { checkoutResult: CheckoutResult }) {

-

- View details -

+

View details

- // - //
- // ); - // } - - // if (error) { - // return
Something went wrong...
; - // } const intervals = Array.from( new Set( @@ -78,10 +68,17 @@ export default function PricingTable({ product.scenario === "scheduled", onClick: async () => { + if (!stripeConnected) { + toast.error("Please connect your Stripe account first"); + return; + } + if (product.id) { await checkout({ productId: product.id, dialog: CheckoutDialog, + openInNewTab: true, + successUrl: `${window.location.origin}`, }); } else if (product.display?.button_url) { window.open(product.display?.button_url, "_blank"); diff --git a/vite/src/components/general/modal-components/DialogContentWrapper.tsx b/vite/src/components/general/modal-components/DialogContentWrapper.tsx index c4d318bec..0637f743c 100644 --- a/vite/src/components/general/modal-components/DialogContentWrapper.tsx +++ b/vite/src/components/general/modal-components/DialogContentWrapper.tsx @@ -1,7 +1,59 @@ -export const DialogContentWrapper = ({ +import { DialogContent } from "@/components/ui/dialog"; +import { cn } from "@/lib/utils"; + +export const CustomDialogContent = ({ children, + fromTop = true, + setStep, + className, }: { children: React.ReactNode; + fromTop?: boolean; + setStep?: (step: any) => void; + className?: string; }) => { - return
{children}
; + return ( + + {children} + + ); +}; + +export const CustomDialogBody = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+ {children} +
+ ); +}; + +export const CustomDialogFooter = ({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) => { + return ( +
+ {children} +
+ ); }; diff --git a/vite/src/components/ui/button.tsx b/vite/src/components/ui/button.tsx index ef8ac3440..d78fcbf9a 100644 --- a/vite/src/components/ui/button.tsx +++ b/vite/src/components/ui/button.tsx @@ -52,6 +52,7 @@ const buttonVariants = cva( "bg-gradient-to-b font-semibold border-t border-red-400 outline outline-red-500 rounded-sm from-red-500/85 to-red-500 text-white hover:from-red-500 hover:to-red-500 shadow-red-500/50 transition-[background] duration-300 !h-7.5 mt-0.25", auth: "!gap-2 hover:bg-stone-100 border border-zinc-250 bg-white text-t1 w-full shadow-sm", + dialogBack: "hover:!bg-zinc-200 p-1 !h-6 ml-5 text-t3 rounded-md", }, size: { default: "h-8 px-3 flex items-center gap-1", diff --git a/vite/src/utils/formatUtils/formatUtils.ts b/vite/src/utils/formatUtils/formatUtils.ts new file mode 100644 index 000000000..9d5f4c345 --- /dev/null +++ b/vite/src/utils/formatUtils/formatUtils.ts @@ -0,0 +1,27 @@ +function stringToSnakeCase(str: string): string { + return str + .replace(/([a-z])([A-Z])/g, "$1_$2") + .replace(/[-\s]+/g, "_") + .toLowerCase(); +} + +export const toSnakeCase = (obj: any, excludeKeys?: string[]): any => { + if (Array.isArray(obj)) { + return obj.map((item) => toSnakeCase(item, excludeKeys)); + } else if (obj !== null && typeof obj === "object") { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => { + const snakeKey = stringToSnakeCase(key); + + // If this key should be excluded, convert the key but keep the value as-is + if (excludeKeys && excludeKeys.includes(key)) { + return [snakeKey, value]; + } + + // Otherwise, convert both key and recursively process the value + return [snakeKey, toSnakeCase(value, excludeKeys)]; + }) + ); + } + return obj; +}; diff --git a/vite/src/views/credits/CreateCreditSystem.tsx b/vite/src/views/credits/CreateCreditSystem.tsx index 9283d8a74..1bee7153d 100644 --- a/vite/src/views/credits/CreateCreditSystem.tsx +++ b/vite/src/views/credits/CreateCreditSystem.tsx @@ -21,6 +21,11 @@ import { import { getBackendErr } from "@/utils/genUtils"; import CreditSystemConfig from "./CreditSystemConfig"; import { useFeaturesContext } from "../features/FeaturesContext"; +import { + CustomDialogBody, + CustomDialogContent, + CustomDialogFooter, +} from "@/components/general/modal-components/DialogContentWrapper"; const defaultCreditSystem = { name: "", id: "", @@ -32,7 +37,7 @@ const defaultCreditSystem = { }; export const validateCreditSystem = ( - creditSystem: CreateFeature, + creditSystem: CreateFeature ): string | null => { if (!creditSystem.id || !creditSystem.name) { return "Please fill in all fields"; @@ -99,25 +104,30 @@ function CreateCreditSystem() { - - - Create Credit System - - + + + + Create Credit System + + + - + - - + + + {/* + + */}
); } diff --git a/vite/src/views/credits/CreditSystemConfig.tsx b/vite/src/views/credits/CreditSystemConfig.tsx index be36bb027..2b82597be 100644 --- a/vite/src/views/credits/CreditSystemConfig.tsx +++ b/vite/src/views/credits/CreditSystemConfig.tsx @@ -38,7 +38,7 @@ function CreditSystemConfig({ : { name: "", id: "", - }, + } ); const [idChanged, setIdChanged] = useState(creditSystem.name !== ""); const [creditSystemConfig, setCreditSystemConfig] = useState( @@ -52,7 +52,7 @@ function CreditSystemConfig({ credit_amount: 0, }, ], - }, + } ); const handleSchemaChange = (index: number, key: string, value: any) => { @@ -89,8 +89,8 @@ function CreditSystemConfig({ }, [fields, creditSystemConfig]); return ( -
-
+
+
Name
-
+
Metered Feature Credit Amount
-
+
{creditSystemConfig.schema.map((item: any, index: number) => (
-
+
-
+
@@ -197,7 +197,7 @@ function CreditSystemConfig({ disabled={ creditSystemConfig.schema.length == features.filter( - (feature: Feature) => feature.type === FeatureType.Metered, + (feature: Feature) => feature.type === FeatureType.Metered ).length } > diff --git a/vite/src/views/credits/UpdateCreditSystem.tsx b/vite/src/views/credits/UpdateCreditSystem.tsx index e17d21105..ee2f9ebed 100644 --- a/vite/src/views/credits/UpdateCreditSystem.tsx +++ b/vite/src/views/credits/UpdateCreditSystem.tsx @@ -13,6 +13,11 @@ import { useAxiosInstance } from "@/services/useAxiosInstance"; import { toast } from "sonner"; import { useFeaturesContext } from "../features/FeaturesContext"; import { validateCreditSystem } from "./CreateCreditSystem"; +import { + CustomDialogBody, + CustomDialogContent, + CustomDialogFooter, +} from "@/components/general/modal-components/DialogContentWrapper"; function UpdateCreditSystem({ open, @@ -43,7 +48,7 @@ function UpdateCreditSystem({ selectedCreditSystem.id, { ...selectedCreditSystem, - }, + } ); await mutate(); setOpen(false); @@ -55,24 +60,26 @@ function UpdateCreditSystem({ return ( - - Update Credit System + + + Update Credit System - + + - + - - + + ); } diff --git a/vite/src/views/features/CreateFeature.tsx b/vite/src/views/features/CreateFeature.tsx index eef31c992..1cb417374 100644 --- a/vite/src/views/features/CreateFeature.tsx +++ b/vite/src/views/features/CreateFeature.tsx @@ -20,52 +20,41 @@ import { FeatureService } from "@/services/FeatureService"; import { FeatureConfig } from "./metered-features/FeatureConfig"; import { getBackendErr } from "@/utils/genUtils"; import { useEnv } from "@/utils/envUtils"; +import { getDefaultFeature } from "./utils/defaultFeature"; +import { + CustomDialogBody, + CustomDialogContent, +} from "@/components/general/modal-components/DialogContentWrapper"; +import { CreateFeatureFooter } from "./components/CreateFeatureFooter"; export const CreateFeature = ({ - isFromEntitlement, - setShowFeatureCreate, - setSelectedFeature, + // isFromEntitlement, + // setShowFeatureCreate, + onSuccess, setOpen, open, entityCreate, + handleBack, }: { - isFromEntitlement: boolean; - setShowFeatureCreate: (show: boolean) => void; - setSelectedFeature: (feature: CreateFeatureType) => void; + // isFromEntitlement: boolean; + // setShowFeatureCreate: (show: boolean) => void; + onSuccess?: (newFeature: CreateFeatureType) => Promise; setOpen: (open: boolean) => void; open: boolean; entityCreate?: boolean; + handleBack?: () => void; }) => { - const { mutate, features } = useFeaturesContext(); const env = useEnv(); - const defaultFeature: CreateFeatureType = { - type: FeatureType.Metered, - config: { - filters: [ - { - property: "", - operator: "", - value: [], - }, - ], - usage_type: entityCreate - ? FeatureUsageType.Continuous - : FeatureUsageType.Single, - }, - name: "", - id: "", - }; const axiosInstance = useAxiosInstance({ env }); - - const [loading, setLoading] = useState(false); - const [feature, setFeature] = useState(defaultFeature); + const { mutate, features } = useFeaturesContext(); + const [feature, setFeature] = useState(getDefaultFeature(entityCreate)); const [eventNameInput, setEventNameInput] = useState(""); const [eventNameChanged, setEventNameChanged] = useState(true); useEffect(() => { if (open) { - setFeature(defaultFeature); + setFeature(getDefaultFeature(entityCreate)); } }, [open]); @@ -90,7 +79,6 @@ export const CreateFeature = ({ feature.config = updateConfig(); - setLoading(true); try { const { data: createdFeature } = await FeatureService.createFeature( axiosInstance, @@ -99,45 +87,52 @@ export const CreateFeature = ({ id: feature.id, type: feature.type, config: updateConfig(), - }, + } ); - if (isFromEntitlement) { - if (createdFeature) { - setSelectedFeature(createdFeature); - } - setShowFeatureCreate(false); + if (onSuccess) { + await onSuccess(createdFeature); } else { await mutate(); setOpen(false); } + + // if (isFromEntitlement) { + // if (createdFeature) { + // setSelectedFeature(createdFeature); + // } + // setShowFeatureCreate(false); + // } else { + // await mutate(); + // setOpen(false); + // } } catch (error) { toast.error(getBackendErr(error, "Failed to create feature")); } - setLoading(false); }; return ( -
- + + + Create Feature + +
+ +
+
+ - - - -
+ ); }; @@ -149,18 +144,34 @@ export const CreateFeatureDialog = () => { - - - Create Feature - - {}} - setSelectedFeature={() => {}} - setOpen={setOpen} - open={open} - /> - + + + ); }; + +// +// +// Create Feature +// +// {/* {}} +// setSelectedFeature={() => {}} +// setOpen={setOpen} +// open={open} +// /> */} +// +{ + /* + + */ +} diff --git a/vite/src/views/features/UpdateFeature.tsx b/vite/src/views/features/UpdateFeature.tsx index 64ab8f518..ac3610606 100644 --- a/vite/src/views/features/UpdateFeature.tsx +++ b/vite/src/views/features/UpdateFeature.tsx @@ -13,6 +13,12 @@ import { useAxiosInstance } from "@/services/useAxiosInstance"; import { toast } from "sonner"; import { FeatureType } from "@autumn/shared"; import { getBackendErr } from "@/utils/genUtils"; +import { + CustomDialogBody, + CustomDialogContent, + CustomDialogFooter, +} from "@/components/general/modal-components/DialogContentWrapper"; +import { CircleArrowUp, Save } from "lucide-react"; export default function UpdateFeature({ open, @@ -80,6 +86,36 @@ export default function UpdateFeature({ setUpdateLoading(false); }; + return ( + + + + Update Feature + + + + + + + + + ); + return ( diff --git a/vite/src/views/features/components/CreateFeatureFooter.tsx b/vite/src/views/features/components/CreateFeatureFooter.tsx new file mode 100644 index 000000000..f13c2c99b --- /dev/null +++ b/vite/src/views/features/components/CreateFeatureFooter.tsx @@ -0,0 +1,41 @@ +import { CustomDialogFooter } from "@/components/general/modal-components/DialogContentWrapper"; +import { Button } from "@/components/ui/button"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { ArrowLeft } from "lucide-react"; +import { useState } from "react"; + +export const CreateFeatureFooter = ({ + handleBack, + handleCreate, +}: { + handleBack?: () => void; + handleCreate: () => Promise; +}) => { + const [loading, setLoading] = useState(false); + return ( + + {handleBack ? ( + + ) : ( +
+ )} + +
+ ); +}; diff --git a/vite/src/views/features/hooks/useFeatureDialogState.tsx b/vite/src/views/features/hooks/useFeatureDialogState.tsx new file mode 100644 index 000000000..b5cc66a1d --- /dev/null +++ b/vite/src/views/features/hooks/useFeatureDialogState.tsx @@ -0,0 +1,21 @@ +import { useState } from "react"; +import { getDefaultFeature } from "../utils/defaultFeature"; + +export const useFeatureDialogState = ({ + entityCreate, +}: { + entityCreate?: boolean; +}) => { + const [feature, setFeature] = useState(getDefaultFeature(entityCreate)); + const [eventNameInput, setEventNameInput] = useState(""); + const [eventNameChanged, setEventNameChanged] = useState(true); + + return { + feature, + setFeature, + eventNameInput, + setEventNameInput, + eventNameChanged, + setEventNameChanged, + }; +}; diff --git a/vite/src/views/features/metered-features/FeatureConfig.tsx b/vite/src/views/features/metered-features/FeatureConfig.tsx index bc2684e0d..f81004a13 100644 --- a/vite/src/views/features/metered-features/FeatureConfig.tsx +++ b/vite/src/views/features/metered-features/FeatureConfig.tsx @@ -167,7 +167,7 @@ export function FeatureConfig({
Name { const newFields: any = { ...fields, name: e.target.value }; diff --git a/vite/src/views/features/utils/defaultFeature.ts b/vite/src/views/features/utils/defaultFeature.ts new file mode 100644 index 000000000..f0dfbf411 --- /dev/null +++ b/vite/src/views/features/utils/defaultFeature.ts @@ -0,0 +1,21 @@ +import { CreateFeature, FeatureType, FeatureUsageType } from "@autumn/shared"; + +export const getDefaultFeature = (entityCreate?: boolean): CreateFeature => { + return { + type: FeatureType.Metered, + config: { + filters: [ + { + property: "", + operator: "", + value: [], + }, + ], + usage_type: entityCreate + ? FeatureUsageType.Continuous + : FeatureUsageType.Single, + }, + name: "", + id: "", + }; +}; diff --git a/vite/src/views/onboarding2/OnboardingView2.tsx b/vite/src/views/onboarding2/OnboardingView2.tsx index ce9ba3d00..9552c749e 100644 --- a/vite/src/views/onboarding2/OnboardingView2.tsx +++ b/vite/src/views/onboarding2/OnboardingView2.tsx @@ -1,131 +1,106 @@ -import { useAxiosPostSWR, useAxiosSWR } from "@/services/useAxiosSwr"; -import { useEnv } from "@/utils/envUtils"; -import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router"; -import { ProductsContext } from "../products/ProductsContext"; -import { PageSectionHeader } from "@/components/general/PageSectionHeader"; -import CreateProduct from "../products/CreateProduct"; -import { ProductV2 } from "@autumn/shared"; -import { ProductsTable } from "../products/ProductsTable"; +import { useAxiosSWR } from "@/services/useAxiosSwr"; import LoadingScreen from "../general/LoadingScreen"; -import { EditProductDialog } from "../onboarding/onboarding-steps/ProductList"; import { AutumnProvider } from "autumn-js/react"; import PricingTable from "@/components/autumn/pricing-table"; -import Install from "./Install"; -import EnvStep from "./Env"; -import MountHandler from "./MountHandler"; -import AutumnProviderStep from "./AutumnProvider"; -import AttachProduct from "./AttachProduct"; -import SmallSpinner from "@/components/general/SmallSpinner"; -import { CustomersTable } from "../customers/CustomersTable"; -import { CustomersContext } from "../customers/CustomersContext"; import { ModelPricing } from "./model-pricing/ModelPricing"; import { useListProducts } from "./model-pricing/usePricingTable"; -import { parseAsString, useQueryStates } from "nuqs"; +import { parseAsBoolean, parseAsString, useQueryStates } from "nuqs"; import IntegrateAutumn from "./integrate/IntegrateAutumn"; +import { useEffect, useRef, useState } from "react"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useSearchParams } from "react-router"; +import { useEnv } from "@/utils/envUtils"; +import { useSession } from "@/lib/auth-client"; export default function OnboardingView2() { const [queryStates, setQueryStates] = useQueryStates({ - page: parseAsString.withDefault("integrate"), + page: parseAsString.withDefault("pricing"), + reactTypescript: parseAsBoolean.withDefault(true), + frontend: parseAsString.withDefault(""), + backend: parseAsString.withDefault(""), + auth: parseAsString.withDefault(""), + customerType: parseAsString.withDefault("user"), + productId: parseAsString.withDefault(""), }); + const [searchParams] = useSearchParams(); + const token = searchParams.get("token"); + const [loading, setLoading] = useState(true); + const axiosInstance = useAxiosInstance(); + const hasHandledToken = useRef(false); + const { data } = useSession(); + const orgId = data?.session?.activeOrganizationId; + const { products: autumnProducts, isLoading: isAutumnLoading, - error, mutate: mutateAutumnProducts, - } = useListProducts(); + } = useListProducts({ customerId: "onboarding_demo_user" }); - const { data, mutate, isLoading } = useAxiosSWR({ url: `/products/data` }); - const { data: productCounts } = useAxiosSWR({ url: `/products/counts` }); + const { + data: productsData, + mutate: productMutate, + isLoading, + } = useAxiosSWR({ url: `/products/data` }); - if (isLoading || isAutumnLoading) return ; + const { data: productCounts, mutate: mutateCounts } = useAxiosSWR({ + url: `/products/counts?latest_version=true`, + }); + + useEffect(() => { + const handleToken = async () => { + try { + await axiosInstance.post("/onboarding", { + token, + }); + + await productMutate(); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + if (token && !hasHandledToken.current) { + hasHandledToken.current = true; + handleToken(); + } + }, [searchParams, token, axiosInstance, productMutate]); + + useEffect(() => { + if (orgId && !token) { + setLoading(false); + } + }, [orgId, token]); + + if (isLoading || isAutumnLoading || loading) return ; return ( - + <> {queryStates.page === "integrate" ? ( - + ) : ( - //
- //
- //
- //

Integrate Autumn

- //

- // Let's integrate Autumn and get your first customer onto one of - // your plans - //

- //
- //
- //
{ - await mutate(); + await productMutate(); await mutateAutumnProducts(); }} + mutateAutumnProducts={mutateAutumnProducts} autumnProducts={autumnProducts} productCounts={productCounts} + mutateCounts={mutateCounts} + queryStates={queryStates} + setQueryStates={setQueryStates} /> )} - {/*
-
-
-

Integrate Autumn

-

- Let's integrate Autumn and get your first customer onto one of - your plans -

-
- - -
-
- -

- Create a .env file in the root of your project and add the - following environment variables: -

- -
- - - - -

- If you've made it to this point, you should see a customer (with - the customerId you returned in autumnHandler) here! -

-
- - -

Watching for customers...

-
- } - /> - - - -
-
-
-
*/} - + ); } @@ -140,6 +115,6 @@ const StepHeader = ({ number, title }: { number: number; title: string }) => { ); }; -const SamplePricingTable = () => { - return ; -}; +// const SamplePricingTable = () => { +// return ; +// }; diff --git a/vite/src/views/onboarding2/SampleApp.tsx b/vite/src/views/onboarding2/SampleApp.tsx new file mode 100644 index 000000000..283f4db15 --- /dev/null +++ b/vite/src/views/onboarding2/SampleApp.tsx @@ -0,0 +1,487 @@ +import { Button } from "@/components/ui/button"; +import Step from "@/components/general/OnboardingStep"; +import CheckDialog from "@/components/autumn/paywall-dialog"; + +import { useEnv } from "@/utils/envUtils"; + +import { + DialogHeader, + DialogContent, + DialogTrigger, + DialogTitle, +} from "@/components/ui/dialog"; +import { useEffect, useState } from "react"; +import { Dialog } from "@/components/ui/dialog"; + +import { toast } from "sonner"; + +import { useSearchParams } from "react-router"; + +import { useCustomer, PricingTable } from "autumn-js/react"; +// import PricingTable from "@/components/autumn/pricing-table"; +import { + Check, + Lock, + Send, + ChevronDown, + ChevronRight, + Code, + ArrowUpRightFromSquare, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { useListProducts } from "./model-pricing/usePricingTable"; +// import PaywallDialog from "@/components/autumn/paywall-dialog"; + +export const SampleApp = ({ data }: { data: any }) => { + const env = useEnv(); + const [searchParams] = useSearchParams(); + const token = searchParams.get("token"); + const [open, setOpen] = useState(false); + const [checkData, setCheckData] = useState(null); + const [trackData, setTrackData] = useState(null); + const [showCodeSection, setShowCodeSection] = useState(true); + const [showCheckSnippet, setShowCheckSnippet] = useState(true); + const [showTrackSnippet, setShowTrackSnippet] = useState(true); + const [showCustomerSnippet, setShowCustomerSnippet] = useState(true); + const [lastUsedFeature, setLastUsedFeature] = useState({ + featureId: data.features?.[0]?.id, + value: 1, + }); + + const { customer, openBillingPortal } = useCustomer(); + const { products } = useListProducts({ customerId: "onboarding_demo_user" }); + + return ( + + + + + +
+ + Sample App + + +
+ + Every time you use a feature, we{" "} + + check + {" "} + for access permission, then{" "} + + track + {" "} + the usage. Test using features, hitting usage limits, upgrade and + downgrade flows, and making changes to your products. + +
+
+
+
+
+
+

Available Features

+
+
+ {data.features + ?.filter((feature: any) => customer?.features?.[feature.id]) + .concat( + data.features?.filter( + (feature: any) => !customer?.features?.[feature.id] + ) || [] + ) + .map((feature: any, index: number) => { + const customerFeature = customer?.features?.[feature.id]; + return ( + + ); + })} +
+
+ + {/* Products List */} +
+
+

Billing

+ {customer?.stripe_id && ( + + )} +
+ +
+ +
+
+
+ + {/* Code Snippets Toggle Button */} + {showCodeSection && ( +
+ {/* Code Snippets */} +
+
+
+
+ Check feature + +
+ {showCheckSnippet && ( + { +if ( !allowed({ featureId: '${ + lastUsedFeature?.featureId || + data.features?.[0]?.id || + "feature-id" + }' }) ) { + alert('Feature not allowed'); + } +} ` + : "// Click 'Send' on a feature" + }`, + }, + { + title: "Node.js", + language: "typescript", + displayLanguage: "typescript", + content: `${ + checkData + ? `import { Autumn } from 'autumn-js'; + +const autumn = new Autumn({ + secretKey: 'am_sk_1234567890' +}); + +const { data } = await autumn.check({ + customerId: 'user_123', + featureId: '${ + lastUsedFeature?.featureId || data.features?.[0]?.id || "feature-id" + }' +}); +` + : "// Click 'Send' on a feature" + }`, + }, + { + title: "Response", + language: "typescript", + displayLanguage: "typescript", + content: `${ + checkData + ? JSON.stringify(checkData, null, 2) + : "// Click 'Send' on a feature" + }`, + }, + ]} + /> + )} +
+ +
+
+ Track usage + +
+ {showTrackSnippet && ( + { + await track({ + featureId: '${ + lastUsedFeature?.featureId || data.features?.[0]?.id || "feature-id" + }', + value: ${lastUsedFeature?.value || 1} + }); +};` + : "// Click 'Send' on a feature" + }`, + }, + { + title: "Node.js", + language: "typescript", + displayLanguage: "typescript", + content: `${ + trackData + ? `import { Autumn } from 'autumn-js'; + +const autumn = new Autumn({ + secretKey: 'am_sk_1234567890' +}); + +const response = await autumn.track({ + featureId: '${ + lastUsedFeature?.featureId || data.features?.[0]?.id || "feature-id" + }', + value: ${lastUsedFeature?.value || 1} +}); +` + : "// Click 'Send' on a feature" + }`, + }, + { + title: "Response", + language: "typescript", + displayLanguage: "typescript", + content: `${ + trackData + ? JSON.stringify(trackData, null, 2) + : "// Click 'Send' on a feature" + }`, + }, + ]} + /> + )} +
+ +
+
+ Customer data + +
+ {showCustomerSnippet && ( + { + //refresh customer data after feature is used + await refetch(); +};`, + }, + { + title: "Node.js", + language: "typescript", + displayLanguage: "typescript", + content: `import { Autumn } from 'autumn-js'; + +const autumn = new Autumn({ + secretKey: 'am_sk_1234567890' +}); + +const { customer } = await autumn.customers.get('user_123');`, + }, + { + title: "Response", + language: "typescript", + displayLanguage: "typescript", + content: `${ + customer + ? JSON.stringify(customer, null, 2) + : "// Customer data will appear here" + }`, + }, + ]} + /> + )} +
+
+
+
+ )} +
+
+
+ ); +}; + +const FeatureUsageItem = ({ + feature, + customerFeature, + onCheckData, + onTrackData, + onFeatureUsed, +}: { + feature: any; + customerFeature: any; + onCheckData: (data: any) => void; + onTrackData: (data: any) => void; + onFeatureUsed: (feature: any) => void; +}) => { + const { check, track, refetch } = useCustomer(); + const [trackValue, setTrackValue] = useState(1); + + if (feature.type === "boolean") { + return ( +
+
+ {feature.name || `Feature ${feature.id || "Unknown"}`} + {customerFeature ? ( + + ) : ( + + )} +
+
+ ); + } + return ( +
+
+
+ + {feature.name || `Feature ${feature.id || "Unknown"}`} + + {!customerFeature && } +
+
+
+
+ setTrackValue(e.target.value)} + className="w-12 h-6 px-1 text-xs bg-white border rounded-xs text-center [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + disabled={!customerFeature} + /> + +
+ {customerFeature && ( + + {customerFeature.unlimited + ? "unlimited" + : `${customerFeature.usage}${ + customerFeature.included_usage > 0 + ? ` / ${customerFeature.included_usage}` + : "" + }`} + + )} +
+
+ ); +}; diff --git a/vite/src/views/onboarding2/integrate/AITools.tsx b/vite/src/views/onboarding2/integrate/AITools.tsx index 3b0ec56d2..2d68ab073 100644 --- a/vite/src/views/onboarding2/integrate/AITools.tsx +++ b/vite/src/views/onboarding2/integrate/AITools.tsx @@ -10,11 +10,14 @@ import { AccordionItem, } from "@/components/ui/accordion"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import CodeBlock from "@/views/onboarding/components/CodeBlock"; +import { InfoBox } from "./components/InfoBox"; +import { CodeSpan } from "./components/CodeSpan"; export const AITools = () => { // MCP configuration for Autumn const mcpConfig = { - name: "autumn", + name: "autumn-docs", command: "npx", args: ["-y", "mcp-remote", "https://docs.useautumn.com/mcp"], }; @@ -45,7 +48,7 @@ export const AITools = () => { type="single" collapsible className="w-full" - defaultValue="item-1" + // defaultValue="item-1" > @@ -54,52 +57,64 @@ export const AITools = () => { Add Autumn to your AI tools
- -
- Cursor - + +

+ Install our MCP with Cursor or Claude Code to use AI to integrate + Autumn. +

+
+
+ Cursor + +
+
+ Claude Code + +
+ + When using Cursor or Claude, prompt the model to use the + `autumn-docs` MCP to integrate Autumn. +
-

- If you're using Cursor or Claude Code, you can install our MCP server to - use AI to integrate Autumn. -

{/* One-click install for Cursor */} -
- -
+
); }; diff --git a/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx index d0181ef75..81531e346 100644 --- a/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx +++ b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx @@ -14,22 +14,42 @@ import { import { AddAutumnProvider } from "./integration-steps/AddAutumnProvider"; import { CheckoutPricingTable } from "./integration-steps/CheckoutPricingTable"; import { EnvStep } from "./integration-steps/EnvStep"; +import { ArrowLeftIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { NextSteps } from "./NextSteps"; +import { AutumnProvider } from "autumn-js/react"; -export default function IntegrateAutumn() { - const [queryStates, setQueryStates] = useQueryStates({ - reactTypescript: parseAsBoolean.withDefault(true), - frontend: parseAsString.withDefault(""), - backend: parseAsString.withDefault(""), - auth: parseAsString.withDefault(""), - customerType: parseAsString.withDefault(""), - }); - - const stackSelected = Object.values(queryStates).every(notNullish); +export default function IntegrateAutumn({ + data, + mutate, + queryStates, + setQueryStates, +}: { + data: any; + mutate: any; + queryStates: any; + setQueryStates: any; +}) { + const stackSelected = queryStates.frontend && queryStates.backend; return ( - +
+

Integrate Autumn

@@ -38,8 +58,8 @@ export default function IntegrateAutumn() {

+
- {stackSelected && queryStates.reactTypescript && ( <> @@ -48,18 +68,15 @@ export default function IntegrateAutumn() { + + + )}
- - {/*
- -

- Create a .env file in the root of your project and add the following - environment variables: -

- -
*/}
diff --git a/vite/src/views/onboarding2/integrate/NextSteps.tsx b/vite/src/views/onboarding2/integrate/NextSteps.tsx new file mode 100644 index 000000000..710a72ebb --- /dev/null +++ b/vite/src/views/onboarding2/integrate/NextSteps.tsx @@ -0,0 +1,29 @@ +import { Button } from "@/components/ui/button"; +import { StepHeader } from "./StepHeader"; +import { SampleApp } from "../SampleApp"; +import { useIntegrateContext } from "./IntegrateContext"; + +export const NextSteps = () => { + const { data, mutate } = useIntegrateContext(); + return ( + <> +
+ Next Steps

} /> +

+ Congrats on setting up Autumn! The next steps are to learn how to use + Autumn to check if a user has access to features in your application, + and track usage for those features. Learn how to do so{" "} + + here + + . +

+ + +
+ + ); +}; diff --git a/vite/src/views/onboarding2/integrate/SelectStack.tsx b/vite/src/views/onboarding2/integrate/SelectStack.tsx index 77c8398d5..5ab64984f 100644 --- a/vite/src/views/onboarding2/integrate/SelectStack.tsx +++ b/vite/src/views/onboarding2/integrate/SelectStack.tsx @@ -26,12 +26,14 @@ import { Button } from "@/components/ui/button"; export const SelectStack = () => { const { queryStates, setQueryStates } = useIntegrateContext(); + console.log("queryStates", queryStates); + const tabClassName = `rounded-xs h-8 data-[state=active]:bg-stone-100 data-[state=active]:text-t2 data-[state=active]:shadow-inner data-[state=active]:border`; return (
-

+

Help us customize the integration guide for your specific tech stack. Click{" "} { - const { product, setProduct, mutate } = useProductContext(); + const { product, setProduct, mutate, autoSave } = useProductContext(); const [open, setOpen] = useState(false); const axiosInstance = useAxiosInstance(); @@ -21,7 +21,6 @@ export const AddTrialButton = () => { e.stopPropagation(); setOpen(true); }} - // className={`w-32 flex items-center gap-2`} className="justify-start w-fit p-0 hover:bg-transparent text-t2 font-medium hover:text-t1" > {product?.free_trial ? ( @@ -40,12 +39,15 @@ export const AddTrialButton = () => { ...product, free_trial: null, }); - handleAutoSave({ - axiosInstance, - productId: product.id, - product: { ...product, free_trial: null }, - mutate, - }); + + if (!autoSave) { + handleAutoSave({ + axiosInstance, + productId: product.id, + product: { ...product, free_trial: null }, + mutate, + }); + } }} className="hover:bg-zinc-300 !h-4 !w-4 text-t3 mt-0.5" > diff --git a/vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx b/vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx new file mode 100644 index 000000000..fd78ed5dc --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx @@ -0,0 +1,82 @@ +import { ArrowUpRightFromSquare } from "lucide-react"; + +import Step from "@/components/general/OnboardingStep"; +import { useState } from "react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { OrgService } from "@/services/OrgService"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { AppEnv } from "@autumn/shared"; +import { toast } from "sonner"; +import { getBackendErr } from "@/utils/genUtils"; + +export const ConnectStripeStep = ({ + mutate, + productData, +}: { + mutate: () => Promise; + productData: any; +}) => { + const [testApiKey, setTestApiKey] = useState(""); + + const [loading, setLoading] = useState(false); + + const axiosInstance = useAxiosInstance({ env: AppEnv.Live }); + + const handleConnectStripe = async () => { + setLoading(true); + try { + await OrgService.connectStripe(axiosInstance, { + testApiKey, + liveApiKey: testApiKey, + successUrl: `https://useautumn.com`, + }); + + toast.success("Successfully connected to Stripe"); + await mutate(); + } catch (error) { + console.log("Failed to connect Stripe", error); + toast.error(getBackendErr(error, "Failed to connect Stripe")); + } + + setLoading(false); + }; + + // console.log("productData", productData); + const stripeConnected = productData?.org.stripe_connected; + return ( +

+

+ Connect your Stripe account to checkout and attach a product to a + customer. Grab your secret key here{" "} + + here + + . +

+
+ setTestApiKey(e.target.value)} + disabled={stripeConnected} + /> + + +
+
+ ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx index d01b73d41..9f1320507 100644 --- a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx +++ b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx @@ -1,3 +1,4 @@ +import ConfirmNewVersionDialog from "@/views/products/product/versioning/ConfirmNewVersionDialog"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; import { ToggleButton } from "@/components/general/ToggleButton"; import { Input } from "@/components/ui/input"; @@ -7,59 +8,106 @@ import { CreateFreeTrial } from "@/views/products/product/free-trial/CreateFreeT import { CreateProductItem2 } from "@/views/products/product/product-item/CreateProductItem2"; import { ProductItemTable } from "@/views/products/product/product-item/ProductItemTable"; import { ProductContext } from "@/views/products/product/ProductContext"; -import { Button } from "@/components/ui/button"; import { AddTrialButton } from "./AddTrialButton"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useEnv } from "@/utils/envUtils"; import { handleAutoSave } from "./model-pricing-utils/modelPricingUtils"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { useModelPricingContext } from "./ModelPricingContext"; -import { ProductRowToolbar } from "@/views/products/components/ProductRowToolbar"; +import { isFreeProduct } from "@/utils/product/priceUtils"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { SaveIcon } from "lucide-react"; +import { Product } from "@autumn/shared"; +import { updateProduct } from "@/views/products/product/utils/updateProduct"; +import { getBackendErr } from "@/utils/genUtils"; +import { EditProductDetails } from "./edit-product/EditProductDetails"; + +export const EditProduct = ({ data, mutate }: { data: any; mutate: any }) => { + const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); + const { productCount, productDataState, mutateCounts } = + useModelPricingContext(); + + const [showNewVersionDialog, setShowNewVersionDialog] = useState(false); + const { + product, + setProduct, + hasChanges, + features, + setFeatures, + entityFeatureIds, + setEntityFeatureIds, + actionState, + isNewProduct, + } = productDataState; -export const EditProduct = ({ - data, - mutate, - product, - setProduct, -}: { - data: any; - mutate: any; - product: any; - setProduct: any; -}) => { const [details, setDetails] = useState({ name: product.name, id: product.id, }); - const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); - const [entityFeatureIds, setEntityFeatureIds] = useState([]); - const [features, setFeatures] = useState([]); - const { productCounts, editingNewProduct, setEditingNewProduct } = - useModelPricingContext(); const axiosInstance = useAxiosInstance(); const env = useEnv(); - useEffect(() => { - if (data) { - setFeatures(data.features); + const [saveLoading, setSaveLoading] = useState(false); + + // useEffect(() => { + // if (data) { + // setFeatures(data.features); + // } + // }, [data]); + + const runUpdateProduct = async () => { + setSaveLoading(true); + // await handleCreateProduct(false); + try { + await updateProduct({ + axiosInstance, + product, + mutate, + mutateCount: mutateCounts, + }); + } catch (error) { + toast.error(getBackendErr(error, "Failed to update product")); + } finally { + setSaveLoading(false); } - }, [data]); + }; + + const handleSaveClicked = async () => { + if (productCount?.all > 0) { + setShowNewVersionDialog(true); + return; + } + await runUpdateProduct(); + }; const hasItems = product.items.length > 0; + const hasCustomers = productCount?.all > 0; + const showSaveButton = hasCustomers || product.version > 1; const handleToggleSettings = async (key: string) => { - const curValue = product[key]; + if (!product) return; + + const curValue = product[key as keyof Product]; const newProduct = { ...product, [key]: !curValue }; + // Validate + if (key === "is_default" && !isFreeProduct(product.items)) { + toast.error("Default product must be a free product"); + return; + } + setProduct(newProduct); - handleAutoSave({ - axiosInstance, - productId: product.id ? product.id : details.id, - product: { ...product, [key]: !curValue }, - mutate, - }); + if (!showSaveButton) { + handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { ...product, [key]: !curValue }, + mutate, + }); + } }; return ( @@ -82,74 +130,32 @@ export const EditProduct = ({ entityFeatureIds, setEntityFeatureIds, isOnboarding: true, - autoSave: true, + autoSave: !showSaveButton, }} > +
-
-
- - Name - - { - await handleAutoSave({ - axiosInstance, - productId: product.id ? product.id : details.id, - product: { - ...product, - name: details.name, - id: details.id, - }, - mutate, - }); - setProduct({ - ...product, - name: details.name, - id: details.id, - }); - }} - placeholder="Free Plan" - value={details.name} - onChange={(e) => { - const curProduct = data?.products.find( - (p: any) => p.id === details.id - ); - console.log("Cur product:", curProduct); - const newIdData = editingNewProduct - ? { - id: slugify(e.target.value), - } - : {}; - setDetails({ - ...details, - name: e.target.value, - ...newIdData, - }); - }} - /> -
-
- ID - -
-
-
- -
+ + {showSaveButton && ( + + )}
{ - const curProduct = autumnProducts.length > 0 ? autumnProducts[0] : null; - - const [product, setProduct] = useState( - (curProduct && - data.products.find((p: ProductV2) => p.id === autumnProducts[0].id)) || - (defaultProduct as unknown as ProductV2) - ); + const getCurProduct = () => { + if (queryStates.productId) { + const prod = data.products.find( + (p: Product) => p.id === queryStates.productId + ); + if (prod) { + return prod; + } + } else if (data.products.length > 0) { + return data.products[0]; + } + return defaultProduct; + }; + const curProduct = getCurProduct(); const [firstItemCreated, setFirstItemCreated] = useState( autumnProducts.some((p: Product) => p.items.length > 0) ); @@ -68,59 +77,30 @@ export const ModelPricing = ({ nullish(curProduct) ); - // Get latest product - const getAutumnProducts = () => { - const curProductItems = product.items.map((item: any) => - getProductItemResponse({ - item, - features: data.features, - currency: "USD", - }) - ); + const productDataState = useProductData({ + originalProduct: curProduct as any, + originalFeatures: data.features as any, + }); - return autumnProducts; - // return [product, ...autumnProducts]; + const { product } = productDataState; - // const properties: ProductProperties = { - // has_trial: notNullish(product.free_trial), - // is_free: isFreeProduct(product.items), - // is_one_off: isOneOffProduct(product.items), - // updateable: product.items.some( - // (item: any) => item.usage_model == UsageModel.Prepaid - // ), - // }; + // useEffect(() => { + // if (data) { + // const curProduct = data.products.find( + // (p: Product) => p.id === product.id + // ); - // const latestProduct = { - // ...product, - // items: curProductItems, - // properties, - // }; + // if (!curProduct) { + // if (data.products.length > 0) { + // setProduct(data.products[0]); + // } + // } + // } + // }, [data]); - // const curProducts = autumnProducts.filter( - // (p: Product) => p.id !== product.id - // ); + if (!product) return null; - // if (!firstItemCreated) { - // return []; - // } - - // const newProducts = [latestProduct, ...curProducts] as any; - // return sortProductsV2({ products: newProducts }) as Product[]; - }; - - useEffect(() => { - if (data) { - const curProduct = data.products.find( - (p: Product) => p.id === product.id - ); - - if (!curProduct) { - if (data.products.length > 0) { - setProduct(data.products[0]); - } - } - } - }, [data]); + const stripeConnected = data?.org.stripe_connected; return ( @@ -153,8 +138,12 @@ export const ModelPricing = ({
@@ -164,13 +153,37 @@ export const ModelPricing = ({ className={cn( "w-full px-10 flex flex-col gap-4 items-center transition-all duration-1000 ease-in-out overflow-hidden", firstItemCreated - ? "py-10 max-h-[500px] opacity-100 translate-y-0 rounded-t-xl shadow-[0_-2px_2px_-2px_rgba(0,0,0,0.05)] bg-stone-100 border-t border-zinc-200" + ? `py-10 max-h-[500px] opacity-100 translate-y-0 rounded-t-xl shadow-[0_-2px_2px_-2px_rgba(0,0,0,0.05)] + bg-stone-100 border-t border-zinc-200 pb-6` : "py-0 max-h-0 opacity-0 translate-y-4" )} > -
- -
+ +
+ {!stripeConnected && ( + + )} + +
+ +
+
+
@@ -180,7 +193,10 @@ export const ModelPricing = ({ const NewProductPopover = () => { const [open, setOpen] = useState(false); - const { mutate, data, setProduct } = useModelPricingContext(); + const { + mutate, + productDataState: { setProduct }, + } = useModelPricingContext(); const axiosInstance = useAxiosInstance(); const [details, setDetails] = useState({ @@ -257,3 +273,36 @@ const NewProductPopover = () => { ); }; + +// Get latest product +const getAutumnProducts = () => { + // const curProductItems = product.items.map((item: any) => + // getProductItemResponse({ + // item, + // features: data.features, + // currency: "USD", + // }) + // ); + // return [product, ...autumnProducts]; + // const properties: ProductProperties = { + // has_trial: notNullish(product.free_trial), + // is_free: isFreeProduct(product.items), + // is_one_off: isOneOffProduct(product.items), + // updateable: product.items.some( + // (item: any) => item.usage_model == UsageModel.Prepaid + // ), + // }; + // const latestProduct = { + // ...product, + // items: curProductItems, + // properties, + // }; + // const curProducts = autumnProducts.filter( + // (p: Product) => p.id !== product.id + // ); + // if (!firstItemCreated) { + // return []; + // } + // const newProducts = [latestProduct, ...curProducts] as any; + // return sortProductsV2({ products: newProducts }) as Product[]; +}; diff --git a/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx b/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx index 176453160..053a84e85 100644 --- a/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx +++ b/vite/src/views/onboarding2/model-pricing/SelectEditProduct.tsx @@ -7,33 +7,69 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; -import { ChevronDownIcon } from "lucide-react"; +import { ChevronDownIcon, Trash } from "lucide-react"; +import { DeleteProductDialog } from "@/views/products/components/DeleteProductDialog"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; export const SelectEditProduct = () => { - const { data, product, setProduct } = useModelPricingContext(); + const { + data, + productDataState: { product, setProduct }, + queryStates, + setQueryStates, + } = useModelPricingContext(); + const [deleteProductOpen, setDeleteProductOpen] = useState(false); + const selectedClassName = "!bg-zinc-100 h-7 border"; if (data.products.length > 3) { return ( - - - - - - {data.products.map((p: any) => { - if (!p.name) { - return null; - } - return ( - setProduct(p)}> - {p.name} - - ); - })} - - + <> + + + + + + + {data.products.map((p: any) => { + if (!p.name) { + return null; + } + return ( + setProduct(p)} + className="flex items-center justify-between group" + > + {p.name} + + + ); + })} + + + ); } @@ -41,24 +77,50 @@ export const SelectEditProduct = () => { "data-[state=active]:bg-stone-200 data-[state=active]:text-t2 data-[state=active]:font-medium"; return ( - - - {data.products.map((p: any) => { - if (!p.name) { - return null; - } - return ( - setProduct(p)} - > - {p.name} - - ); - })} - - + <> + + + + {data.products.map((p: any) => { + if (!p.name) { + return null; + } + + const isSelected = p.id === product.id; + return ( + { + setProduct(p); + setQueryStates({ + productId: p.id, + }); + }} + > + {p.name} + {isSelected && ( + + )} + + ); + })} + + + ); }; diff --git a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx new file mode 100644 index 000000000..aebee5282 --- /dev/null +++ b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx @@ -0,0 +1,75 @@ +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Input } from "@/components/ui/input"; +import { useEffect, useState } from "react"; +import { handleAutoSave } from "../model-pricing-utils/modelPricingUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useModelPricingContext } from "../ModelPricingContext"; +import { slugify } from "@/utils/formatUtils/formatTextUtils"; + +export const EditProductDetails = () => { + const { + editingNewProduct, + productDataState: { product, setProduct }, + mutate, + } = useModelPricingContext(); + + const axiosInstance = useAxiosInstance(); + const [details, setDetails] = useState({ + name: product?.name, + id: product?.id, + }); + + useEffect(() => { + if (product.id) { + setDetails({ + name: product.name, + id: product.id, + }); + } + }, [product]); + + return ( +
+
+ Name + { + await handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { + ...product, + name: details.name, + id: details.id, + }, + mutate, + }); + setProduct({ + ...product, + name: details.name, + id: details.id, + }); + }} + placeholder="Eg. Free Plan" + value={details.name} + onChange={(e) => { + const newIdData = editingNewProduct + ? { + id: slugify(e.target.value), + } + : {}; + setDetails({ + ...details, + name: e.target.value, + ...newIdData, + }); + }} + /> +
+
+ ID + +
+
+ ); +}; diff --git a/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx b/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx index 60352f603..ef0515e8e 100644 --- a/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx +++ b/vite/src/views/onboarding2/model-pricing/usePricingTable.tsx @@ -1,8 +1,8 @@ import { useAxiosSWR } from "@/services/useAxiosSwr"; -export const useListProducts = () => { +export const useListProducts = ({ customerId }: { customerId: string }) => { const { data, isLoading, error, mutate } = useAxiosSWR({ - url: "/v1/products", + url: `/v1/products?customer_id=${customerId}`, options: { refreshInterval: 0, }, diff --git a/vite/src/views/onboarding2/utils/useCustomerReplica.tsx b/vite/src/views/onboarding2/utils/useCustomerReplica.tsx new file mode 100644 index 000000000..d66183248 --- /dev/null +++ b/vite/src/views/onboarding2/utils/useCustomerReplica.tsx @@ -0,0 +1,37 @@ +import { CheckoutParams } from "autumn-js"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { toast } from "sonner"; +import { toSnakeCase } from "@/utils/formatUtils/formatUtils"; + +const cusData = { + customer_id: "demo_123", + customer_data: { + name: "Demo Customer", + email: "demo@example.com", + }, +}; + +export const useCustomerReplica = () => { + const axiosInstance = useAxiosInstance(); + + const checkout = async (params: any) => { + try { + const { data } = await axiosInstance.post("/v1/checkout", { + ...cusData, + ...toSnakeCase(params), + }); + + if (data.url) { + window.open(data.url, "_blank"); + } + return data; + } catch (error: any) { + toast.error(`Failed to checkout: ${error.message}`); + return null; + } + }; + + return { + checkout, + }; +}; diff --git a/vite/src/views/products/product/ManageProduct.tsx b/vite/src/views/products/product/ManageProduct.tsx index 05a3bbabc..7b8960446 100644 --- a/vite/src/views/products/product/ManageProduct.tsx +++ b/vite/src/views/products/product/ManageProduct.tsx @@ -11,7 +11,7 @@ export const ManageProduct = ({ hideAdminHover?: boolean; }) => { const env = useEnv(); - const { isOnboarding, product, entityId, customer } = useProductContext(); + const { product, entityId, customer } = useProductContext(); return (
diff --git a/vite/src/views/products/product/ProductView.tsx b/vite/src/views/products/product/ProductView.tsx index f606f374c..cdb3aa78a 100644 --- a/vite/src/views/products/product/ProductView.tsx +++ b/vite/src/views/products/product/ProductView.tsx @@ -51,32 +51,14 @@ function ProductView({ env }: { env: AppEnv }) { setEntityFeatureIds, actionState, isNewProduct, - } = useProductData({ data }); + } = useProductData({ + originalProduct: data?.product as any, + originalFeatures: data?.features as any, + }); - // Replace the current useBlocker call with a fixed useEffect const { modal } = useProductChangedAlert({ hasChanges }); - const [buttonLoading, setButtonLoading] = useState(false); - // const hasCustomers = counts?.all > 0; - - // const handleAutoSave = async () => { - // setButtonLoading(true); - // try { - // console.log("Auto saving"); - // await new Promise((resolve) => setTimeout(resolve, 1000)); - // await updateProduct(); - // } catch (error) { - // toast.error(getBackendErr(error, "Failed to auto save product")); - // } - // setButtonLoading(false); - // }; - // useEffect(() => { - // if (hasChanges && !hasCustomers) { - // handleAutoSave(); - // } - // }, [hasChanges]); - if (isLoading) return ; if (!product) { @@ -153,7 +135,6 @@ function ProductView({ env }: { env: AppEnv }) { entityFeatureIds, setEntityFeatureIds, hasChanges, - buttonLoading, setButtonLoading, }} diff --git a/vite/src/views/products/product/hooks/useProductData.tsx b/vite/src/views/products/product/hooks/useProductData.tsx index ff0b4887e..21fac3351 100644 --- a/vite/src/views/products/product/hooks/useProductData.tsx +++ b/vite/src/views/products/product/hooks/useProductData.tsx @@ -3,7 +3,13 @@ import { sortProductItems } from "@/utils/productUtils"; import { AppEnv, Feature, ProductItem, ProductV2 } from "@autumn/shared"; import { useEffect, useRef, useState } from "react"; -export const useProductData = ({ data }: { data: any }) => { +export const useProductData = ({ + originalProduct, + originalFeatures, +}: { + originalProduct: ProductV2 | null; + originalFeatures: Feature[] | null; +}) => { const initialProductRef = useRef(null); const [hasChanges, setHasChanges] = useState(false); const [product, setProduct] = useState(null); @@ -16,16 +22,16 @@ export const useProductData = ({ data }: { data: any }) => { new Set( product.items .filter((item: ProductItem) => item.entity_feature_id != null) - .map((item: ProductItem) => item.entity_feature_id!), - ), + .map((item: ProductItem) => item.entity_feature_id!) + ) ); }; useEffect(() => { - if (data?.product) { + if (originalProduct) { const sortedProduct = { - ...data.product, - items: sortProductItems(data.product.items), + ...originalProduct, + items: sortProductItems(originalProduct.items), }; initialProductRef.current = structuredClone(sortedProduct); @@ -33,10 +39,10 @@ export const useProductData = ({ data }: { data: any }) => { setProduct(sortedProduct); } - if (data?.features) { - setFeatures(data.features); + if (originalFeatures) { + setFeatures(originalFeatures); } - }, [data]); + }, [originalProduct, originalFeatures]); useEffect(() => { if (!product) return; diff --git a/vite/src/views/products/product/prices/CreateFixedPrice.tsx b/vite/src/views/products/product/prices/CreateFixedPrice.tsx index 19d626909..7d74270d1 100644 --- a/vite/src/views/products/product/prices/CreateFixedPrice.tsx +++ b/vite/src/views/products/product/prices/CreateFixedPrice.tsx @@ -42,7 +42,7 @@ function CreateFixedPrice({ (item: ProductItem, index: number) => { const isSameItem = selectedIndex && selectedIndex == index; return !isSameItem && isPriceItem(item) && item.interval; - }, + } ); const newVariantMap: Record = { @@ -123,7 +123,7 @@ function CreateFixedPrice({ step="any" className="h-full !text-lg min-w-36" /> - + {org?.default_currency?.toUpperCase() || "USD"}
diff --git a/vite/src/views/products/product/product-item/CreateProductItem.tsx b/vite/src/views/products/product/product-item/CreateProductItem.tsx index 3965bc517..d828f40b2 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem.tsx @@ -1,16 +1,8 @@ import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; +import { Dialog, DialogTrigger } from "@/components/ui/dialog"; import { useState } from "react"; -import { ProductItemConfig } from "./ProductItemConfig"; import { ProductItemContext } from "./ProductItemContext"; -import { CreateFeature } from "@/views/features/CreateFeature"; import { ProductItemInterval, @@ -20,9 +12,11 @@ import { import { useProductContext } from "../ProductContext"; import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; -import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; -import { ItemConfigFooter } from "./product-item-config/item-config-footer/ItemConfigFooter"; import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { CreateItemDialogContent } from "./create-product-item/CreateItemDialogContent"; +import { Plus } from "lucide-react"; +import { useSteps } from "./useSteps"; +import { CreateItemStep } from "./utils/CreateItemStep"; export const defaultProductItem: ProductItem = { feature_id: null, @@ -64,13 +58,13 @@ export function CreateProductItem() { const { features, product, setProduct, setFeatures, counts, mutate } = useProductContext(); - const axiosInstance = useAxiosInstance(); - const hasCustomers = counts?.all > 0; + // const axiosInstance = useAxiosInstance(); + // const hasCustomers = counts?.all > 0; - const setSelectedFeature = (feature: CreateFeatureType) => { - setFeatures([...features, feature]); - setItem({ ...item, feature_id: feature.id! }); - }; + // const setSelectedFeature = (feature: CreateFeatureType) => { + // setFeatures([...features, feature]); + // setItem({ ...item, feature_id: feature.id! }); + // }; const handleCreateProductItem = async (entityFeatureId?: string) => { const validatedItem = validateProductItem({ @@ -94,6 +88,10 @@ export function CreateProductItem() { setOpen(false); }; + const stepState = useSteps({ + initialStep: CreateItemStep.SelectItemType, + }); + return ( @@ -110,13 +109,21 @@ export function CreateProductItem() { - +
+ + + + ); +} + +{ + /* - -
- + */ +} +{ + /*
@@ -173,8 +181,5 @@ export function CreateProductItem() { ) : ( {}} /> )} - - - - ); + */ } diff --git a/vite/src/views/products/product/product-item/CreateProductItem2.tsx b/vite/src/views/products/product/product-item/CreateProductItem2.tsx index be9850657..2fda08df5 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem2.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem2.tsx @@ -1,16 +1,8 @@ import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; +import { Dialog, DialogTrigger } from "@/components/ui/dialog"; -import { useEffect, useState } from "react"; -import { ProductItemConfig } from "./ProductItemConfig"; +import { useState } from "react"; import { ProductItemContext } from "./ProductItemContext"; -import { CreateFeature } from "@/views/features/CreateFeature"; import { ProductItemInterval, @@ -20,12 +12,11 @@ import { import { useProductContext } from "../ProductContext"; import { validateProductItem } from "@/utils/product/product-item/validateProductItem"; -import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; -import { ItemConfigFooter } from "./product-item-config/item-config-footer/ItemConfigFooter"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; import { PlusIcon } from "lucide-react"; import { CreateItemDialogContent } from "./create-product-item/CreateItemDialogContent"; import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelPricingContext"; +import { useSteps } from "./useSteps"; +import { CreateItemStep } from "./utils/CreateItemStep"; const defaultProductItem: ProductItem = { feature_id: null, @@ -48,8 +39,10 @@ export function CreateProductItem2() { const [open, setOpen] = useState(false); const [showCreateFeature, setShowCreateFeature] = useState(false); const [item, setItem] = useState(defaultProductItem); - const { features, product, setProduct, setFeatures } = useProductContext(); - const { firstItemCreated, setFirstItemCreated } = useModelPricingContext(); + const { features, product, setProduct } = useProductContext(); + const { setFirstItemCreated } = useModelPricingContext(); + + const stepState = useSteps({ initialStep: CreateItemStep.SelectItemType }); const handleCreateProductItem = async (entityFeatureId?: string) => { const validatedItem = validateProductItem({ @@ -88,11 +81,11 @@ export function CreateProductItem2() { setShowCreateFeature, isUpdate: false, handleCreateProductItem, - features, - setFeatures, + open, setOpen, autoSave: true, + stepState, }} > diff --git a/vite/src/views/products/product/product-item/EntitiesDropdown.tsx b/vite/src/views/products/product/product-item/EntitiesDropdown.tsx index 1a87437e0..3b2824b9f 100644 --- a/vite/src/views/products/product/product-item/EntitiesDropdown.tsx +++ b/vite/src/views/products/product/product-item/EntitiesDropdown.tsx @@ -23,6 +23,7 @@ import { CheckIcon, PlusIcon } from "lucide-react"; import { CreateFeature } from "@/views/features/CreateFeature"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; +import { CustomDialogContent } from "@/components/general/modal-components/DialogContentWrapper"; export const EntitiesDropdown = ({ open, @@ -55,7 +56,7 @@ export const EntitiesDropdownContent = () => { const continuousUseFeatures = features.filter( (feature: Feature) => - feature.config?.usage_type === FeatureUsageType.Continuous, + feature.config?.usage_type === FeatureUsageType.Continuous ); return ( @@ -71,12 +72,12 @@ export const EntitiesDropdownContent = () => { const itemsUsingEntity = product.items?.filter( (productItem: ProductItem) => - productItem.entity_feature_id === item.id, + productItem.entity_feature_id === item.id ) || []; if (itemsUsingEntity.length > 0) { toast.error( - "Please delete all items under this entity first", + "Please delete all items under this entity first" ); return currentIds; } @@ -110,19 +111,22 @@ export const EntitiesDropdownContent = () => { - - - Create Entity - + {}} - setSelectedFeature={() => {}} + // isFromEntitlement={false} + // setShowFeatureCreate={() => {}} + // setSelectedFeature={() => {}} setOpen={setCreateFeatureOpen} open={createFeatureOpen} entityCreate={true} /> - + + {/* + + Create Entity + + + */} ); diff --git a/vite/src/views/products/product/product-item/ProductItemTable.tsx b/vite/src/views/products/product/product-item/ProductItemTable.tsx index 8441895b6..42c94a6de 100644 --- a/vite/src/views/products/product/product-item/ProductItemTable.tsx +++ b/vite/src/views/products/product/product-item/ProductItemTable.tsx @@ -21,8 +21,9 @@ import { CreateFreeTrial } from "../free-trial/CreateFreeTrial"; import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; export const ProductItemTable = () => { - const { product, features, org, entityFeatureIds, isOnboarding } = + const { product, features, org, entityFeatureIds, isOnboarding, autoSave } = useProductContext(); + const [selectedItem, setSelectedItem] = useState(null); const [selectedIndex, setSelectedIndex] = useState(null); const [open, setOpen] = useState(false); @@ -67,7 +68,6 @@ export const ProductItemTable = () => {
- {/*
*/} {!isOnboarding && } @@ -110,14 +110,6 @@ export const ProductItemTable = () => { {/*
*/}
- - {/* */}
- + + +
+ Update Item + {selectedItem?.feature_id && ( + + {selectedItem.feature_id || ""} + + )} +
+ +
+ + +
+ + {/*
Update Item @@ -81,7 +101,7 @@ export default function UpdateProductItem({ - + */}
); diff --git a/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx b/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx index 309b320ac..f411fd168 100644 --- a/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx +++ b/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx @@ -5,6 +5,8 @@ import { useProductContext } from "../../ProductContext"; import { FeatureType } from "@autumn/shared"; import { getFeature } from "@/utils/product/entitlementUtils"; import { FeatureConfig } from "../product-item-config/FeatureItemConfig"; +import { useEffect } from "react"; +import { CreateItemStep } from "../utils/CreateItemStep"; export const ConfigWithFeature = ({ show, @@ -16,7 +18,7 @@ export const ConfigWithFeature = ({ handleAddPrice: () => void; }) => { const { features } = useProductContext(); - const { item, setItem } = useProductItemContext(); + const { item } = useProductItemContext(); const isBooleanFeature = getFeature(item.feature_id, features)?.type === FeatureType.Boolean; diff --git a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx index b5dead984..f73b0215b 100644 --- a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx +++ b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx @@ -13,6 +13,8 @@ import { X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { PlusIcon } from "lucide-react"; import { getItemType } from "@/utils/product/productItemUtils"; +import { CreateItemStep } from "../utils/CreateItemStep"; +import { useEffect, useState } from "react"; export const SelectItemFeature = ({ show, @@ -22,14 +24,21 @@ export const SelectItemFeature = ({ setShow: any; }) => { const { features } = useProductContext(); - const { item, setItem, setShowCreateFeature, isUpdate } = - useProductItemContext(); - + const { item, setItem, isUpdate, stepState } = useProductItemContext(); + const [open, setOpen] = useState(false); const itemType = getItemType(item); + // useEffect(() => { + // if (stepState.previousStep === CreateItemStep.SelectItemType) { + // setOpen(true); + // } + // }, [stepState.previousStep]); + return (
void; + pushStep: (step: CreateItemStep) => void; +}) => { + const { features } = useProductContext(); + const { item } = useProductItemContext(); + + return ( + <> + + Select a feature +
+ {features + .filter((feature: Feature) => { + if (isFeaturePriceItem(item)) { + return feature.type !== FeatureType.Boolean; + } + return true; + }) + .map((feature: Feature, index: number) => ( +
{ + // setItem({ ...item, feature_id: feature.id! }); + }} + > + {feature.name} + +
+ ))} +
+
+ +
+
+ + + + + ); +}; diff --git a/vite/src/views/products/product/product-item/product-item-config/item-config-footer/ItemConfigFooter.tsx b/vite/src/views/products/product/product-item/product-item-config/item-config-footer/ItemConfigFooter.tsx index 5db8705fa..2f44c3d9f 100644 --- a/vite/src/views/products/product/product-item/product-item-config/item-config-footer/ItemConfigFooter.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/item-config-footer/ItemConfigFooter.tsx @@ -7,11 +7,12 @@ import { useProductContext } from "../../../ProductContext"; import { AddToEntityDropdown } from "./AddToEntityDropdown"; import { handleAutoSave } from "@/views/onboarding2/model-pricing/model-pricing-utils/modelPricingUtils"; import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { CustomDialogFooter } from "@/components/general/modal-components/DialogContentWrapper"; export const ItemConfigFooter = ({ - setIntroDone, + handleBack, }: { - setIntroDone?: (introDone: boolean) => void; + handleBack?: () => void; }) => { const axiosInstance = useAxiosInstance(); const { entityFeatureIds, product, mutate, autoSave } = useProductContext(); @@ -32,22 +33,18 @@ export const ItemConfigFooter = ({ const showIntro = product.items.length === 0; return ( -
- {showIntro && setIntroDone ? ( + + {handleBack ? ( ) : ( -
+
)}
@@ -77,8 +74,6 @@ export const ItemConfigFooter = ({ variant="add" onClick={async () => { const newProduct = await handleUpdateProductItem(); - console.log("New product:", newProduct); - console.log("Auto save:", autoSave); if (autoSave && newProduct) { handleAutoSave({ @@ -117,7 +112,7 @@ export const ItemConfigFooter = ({ )}
-
+
); }; diff --git a/vite/src/views/products/product/product-item/useSteps.tsx b/vite/src/views/products/product/product-item/useSteps.tsx new file mode 100644 index 000000000..8daa927bd --- /dev/null +++ b/vite/src/views/products/product/product-item/useSteps.tsx @@ -0,0 +1,56 @@ +import { useEffect, useState } from "react"; +import { CreateItemStep } from "./utils/CreateItemStep"; + +export const useSteps = ({ initialStep }: { initialStep: CreateItemStep }) => { + const [stepVal, setStepVal] = useState(initialStep); + const [stepStack, setStepStack] = useState([initialStep]); + + // useEffect(() => { + // setStepStack([initialStep]); + // setStepVal(initialStep); + // }, []); + + const popStep = () => { + if (stepStack.length === 1) { + return; + } + + setStepStack((prev) => { + const newStack = prev.slice(0, -1); + const newStep = newStack[newStack.length - 1]; + setStepVal(newStep); + return newStack; + }); + }; + + const pushStep = (step: CreateItemStep) => { + console.log("Pushing step!", step); + setStepStack((prev) => [...prev, step]); + setStepVal(step); + }; + + const resetSteps = () => { + setStepStack([initialStep]); + setStepVal(initialStep); + }; + + const replaceStep = (step: CreateItemStep) => { + const curStack = stepStack; + if (curStack.length <= 1) { + setStepStack([step]); + setStepVal(step); + } else { + setStepStack((prev) => [...prev.slice(0, -1), step]); + setStepVal(step); + } + }; + + return { + stepVal, + popStep, + pushStep, + resetSteps, + replaceStep, + previousStep: stepStack.length > 1 ? stepStack[stepStack.length - 2] : null, + }; +}; diff --git a/vite/src/views/products/product/product-item/utils/CreateItemStep.tsx b/vite/src/views/products/product/product-item/utils/CreateItemStep.tsx new file mode 100644 index 000000000..3e3c7a1c5 --- /dev/null +++ b/vite/src/views/products/product/product-item/utils/CreateItemStep.tsx @@ -0,0 +1,6 @@ +export enum CreateItemStep { + SelectItemType = "select_item_type", + CreateFeature = "create_feature", + SelectFeature = "select_feature", + CreateItem = "create_item", +} diff --git a/vite/src/views/products/product/utils/updateProduct.ts b/vite/src/views/products/product/utils/updateProduct.ts new file mode 100644 index 000000000..87f80eafb --- /dev/null +++ b/vite/src/views/products/product/utils/updateProduct.ts @@ -0,0 +1,32 @@ +import { ProductService } from "@/services/products/ProductService"; +import { getBackendErr } from "@/utils/genUtils"; +import { Product, ProductV2, UpdateProductSchema } from "@autumn/shared"; +import { AxiosInstance } from "axios"; +import { toast } from "sonner"; + +export const updateProduct = async ({ + axiosInstance, + product, + mutate, + mutateCount, +}: { + axiosInstance: AxiosInstance; + product: ProductV2; + mutate: () => void; + mutateCount: () => void; +}) => { + try { + await ProductService.updateProduct(axiosInstance, product.id, { + ...UpdateProductSchema.parse(product), + items: product.items, + free_trial: product.free_trial, + }); + + toast.success("Product created successfully"); + + await mutate(); + await mutateCount(); + } catch (error) { + toast.error(getBackendErr(error, "Failed to update product")); + } +}; From f2b661ba6da1d6954041818b671146fc1314122b Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 5 Aug 2025 07:39:07 -0700 Subject: [PATCH 10/37] new onboarding done --- vite/src/App.tsx | 8 +- vite/src/components/autumn/pricing-table.tsx | 6 +- vite/src/components/ui/button.tsx | 8 +- .../onboarding-steps/ConnectStripe.tsx | 2 +- .../views/onboarding2/ConnectStripeDialog.tsx | 79 ++++++++++ .../src/views/onboarding2/OnboardingView2.tsx | 41 ++---- .../ConnectStripeStep.tsx} | 22 ++- .../onboarding2/integrate/IntegrateAutumn.tsx | 38 +++-- .../views/onboarding2/integrate/NextSteps.tsx | 2 +- .../onboarding2/integrate/SelectStack.tsx | 23 +-- .../integration-steps/AddAutumnProvider.tsx | 2 +- .../integration-steps/AutumnHandler.tsx | 9 +- .../CheckoutPricingTable.tsx | 2 +- .../integrate/integration-steps/EnvStep.tsx | 2 +- .../integrate/integration-steps/Install.tsx | 2 +- .../integration-steps/snippets/rr7Handler.tsx | 4 - .../onboarding2/model-pricing/EditProduct.tsx | 59 ++++---- .../model-pricing/ModelPricing.tsx | 136 +++++++++--------- .../edit-product/EditProductDetails.tsx | 85 ++++++++--- .../views/onboarding2/utils/connectStripe.ts | 28 ++++ .../product/free-trial/CreateFreeTrial.tsx | 5 +- .../product-item/CreateProductItem2.tsx | 13 +- .../CreateItemDialogContent.tsx | 8 +- .../create-product-item/CreateItemIntro.tsx | 32 +++-- .../item-config-footer/ItemConfigFooter.tsx | 3 +- 25 files changed, 385 insertions(+), 234 deletions(-) create mode 100644 vite/src/views/onboarding2/ConnectStripeDialog.tsx rename vite/src/views/onboarding2/{model-pricing/ConnectStripe.tsx => integrate/ConnectStripeStep.tsx} (75%) create mode 100644 vite/src/views/onboarding2/utils/connectStripe.ts diff --git a/vite/src/App.tsx b/vite/src/App.tsx index 44cb10b99..3ad576dba 100644 --- a/vite/src/App.tsx +++ b/vite/src/App.tsx @@ -33,10 +33,10 @@ export default function App() { } /> } /> - } /> - } /> - } /> - } /> + } /> + } /> + {/* } /> + } /> */} } /> {/* FEATURES */} void; }) { const { mutateAutumnProducts } = useModelPricingContext(); const { checkout } = useCustomer(); @@ -69,7 +71,7 @@ export default function PricingTable({ onClick: async () => { if (!stripeConnected) { - toast.error("Please connect your Stripe account first"); + setConnectStripeOpen(true); return; } @@ -213,7 +215,7 @@ export const PricingCard = ({ return (
void; +}) { + const { stripeConnected, mutate, productDataState, data } = + useModelPricingContext(); + const axiosInstance = useAxiosInstance(); + + const [testApiKey, setTestApiKey] = useState(""); + const [loading, setLoading] = useState(false); + const handleConnectStripe = async () => { + setLoading(true); + await connectStripe({ testApiKey, axiosInstance, mutate }); + setOpen(false); + setLoading(false); + }; + + return ( + + + + + Connect your Stripe account + +

+ To add a product to a customer, first connect your Stripe account. + Grab your secret key{" "} + + here + +

+ {/* */} + setTestApiKey(e.target.value)} + disabled={stripeConnected} + /> +
+ + + +
+
+ ); +} diff --git a/vite/src/views/onboarding2/OnboardingView2.tsx b/vite/src/views/onboarding2/OnboardingView2.tsx index 9552c749e..6ed1b3b4f 100644 --- a/vite/src/views/onboarding2/OnboardingView2.tsx +++ b/vite/src/views/onboarding2/OnboardingView2.tsx @@ -1,7 +1,5 @@ import { useAxiosSWR } from "@/services/useAxiosSwr"; import LoadingScreen from "../general/LoadingScreen"; -import { AutumnProvider } from "autumn-js/react"; -import PricingTable from "@/components/autumn/pricing-table"; import { ModelPricing } from "./model-pricing/ModelPricing"; import { useListProducts } from "./model-pricing/usePricingTable"; import { parseAsBoolean, parseAsString, useQueryStates } from "nuqs"; @@ -9,19 +7,23 @@ import IntegrateAutumn from "./integrate/IntegrateAutumn"; import { useEffect, useRef, useState } from "react"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { useSearchParams } from "react-router"; -import { useEnv } from "@/utils/envUtils"; import { useSession } from "@/lib/auth-client"; export default function OnboardingView2() { - const [queryStates, setQueryStates] = useQueryStates({ - page: parseAsString.withDefault("pricing"), - reactTypescript: parseAsBoolean.withDefault(true), - frontend: parseAsString.withDefault(""), - backend: parseAsString.withDefault(""), - auth: parseAsString.withDefault(""), - customerType: parseAsString.withDefault("user"), - productId: parseAsString.withDefault(""), - }); + const [queryStates, setQueryStates] = useQueryStates( + { + page: parseAsString.withDefault("pricing"), + reactTypescript: parseAsBoolean.withDefault(true), + frontend: parseAsString.withDefault(""), + backend: parseAsString.withDefault(""), + auth: parseAsString.withDefault(""), + customerType: parseAsString.withDefault("user"), + productId: parseAsString.withDefault(""), + }, + { + history: "push", + } + ); const [searchParams] = useSearchParams(); const token = searchParams.get("token"); @@ -103,18 +105,3 @@ export default function OnboardingView2() { ); } - -const StepHeader = ({ number, title }: { number: number; title: string }) => { - return ( -
-
- {number} -
-

{title}

-
- ); -}; - -// const SamplePricingTable = () => { -// return ; -// }; diff --git a/vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx b/vite/src/views/onboarding2/integrate/ConnectStripeStep.tsx similarity index 75% rename from vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx rename to vite/src/views/onboarding2/integrate/ConnectStripeStep.tsx index fd78ed5dc..b70efbc6b 100644 --- a/vite/src/views/onboarding2/model-pricing/ConnectStripe.tsx +++ b/vite/src/views/onboarding2/integrate/ConnectStripeStep.tsx @@ -1,6 +1,3 @@ -import { ArrowUpRightFromSquare } from "lucide-react"; - -import Step from "@/components/general/OnboardingStep"; import { useState } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -9,6 +6,7 @@ import { useAxiosInstance } from "@/services/useAxiosInstance"; import { AppEnv } from "@autumn/shared"; import { toast } from "sonner"; import { getBackendErr } from "@/utils/genUtils"; +import { StepHeader } from "./StepHeader"; export const ConnectStripeStep = ({ mutate, @@ -45,15 +43,12 @@ export const ConnectStripeStep = ({ // console.log("productData", productData); const stripeConnected = productData?.org.stripe_connected; return ( -
-

- Connect your Stripe account to checkout and attach a product to a - customer. Grab your secret key here{" "} - +

+ +

+ Stripe is required to checkout and add your products to customers. Grab + your API key{" "} + here . @@ -62,13 +57,12 @@ export const ConnectStripeStep = ({ setTestApiKey(e.target.value)} disabled={stripeConnected} />

+

Integrate Autumn

Let's integrate Autumn and get your first customer onto one of @@ -63,6 +60,7 @@ export default function IntegrateAutumn({ {stackSelected && queryStates.reactTypescript && ( <> + diff --git a/vite/src/views/onboarding2/integrate/NextSteps.tsx b/vite/src/views/onboarding2/integrate/NextSteps.tsx index 710a72ebb..0f1344970 100644 --- a/vite/src/views/onboarding2/integrate/NextSteps.tsx +++ b/vite/src/views/onboarding2/integrate/NextSteps.tsx @@ -8,7 +8,7 @@ export const NextSteps = () => { return ( <>

- Next Steps

} /> + Next Steps

} />

Congrats on setting up Autumn! The next steps are to learn how to use Autumn to check if a user has access to features in your application, diff --git a/vite/src/views/onboarding2/integrate/SelectStack.tsx b/vite/src/views/onboarding2/integrate/SelectStack.tsx index 5ab64984f..88f0bfd13 100644 --- a/vite/src/views/onboarding2/integrate/SelectStack.tsx +++ b/vite/src/views/onboarding2/integrate/SelectStack.tsx @@ -1,24 +1,5 @@ -import { useState } from "react"; import { StepHeader } from "./StepHeader"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { useIntegrateContext } from "./IntegrateContext"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - Building, - CircleUserRound, - Code, - Fingerprint, - Info, - InfoIcon, - ScanFace, - User, -} from "lucide-react"; import { SelectFrameworks } from "./select-stack/SelectFrameworks"; import { InfoBox } from "./components/InfoBox"; import { Button } from "@/components/ui/button"; @@ -26,13 +7,11 @@ import { Button } from "@/components/ui/button"; export const SelectStack = () => { const { queryStates, setQueryStates } = useIntegrateContext(); - console.log("queryStates", queryStates); - const tabClassName = `rounded-xs h-8 data-[state=active]:bg-stone-100 data-[state=active]:text-t2 data-[state=active]:shadow-inner data-[state=active]:border`; return (

- +

Help us customize the integration guide for your specific tech stack. Click{" "} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx index 8026fdbad..03970611b 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx @@ -113,7 +113,7 @@ export const AddAutumnProvider = () => { return (

Wrap your React app in {""} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx index a3b34292b..8363d2142 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx @@ -153,7 +153,14 @@ export const AutumnHandler = () => { return (
- + + Mount autumnHandler to your backend +

+ } + />

autumnHandler mounts routes on the{" "} /api/autumn/* paths which allows our React hooks diff --git a/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx b/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx index 07c2b17a5..e1f7faa52 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx @@ -28,7 +28,7 @@ export const CheckoutPricingTable = () => { return (

Drop in {""} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx b/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx index 28e682ca8..5a1fd7f93 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx @@ -10,7 +10,7 @@ export const EnvStep = () => { <>
Add the Autumn secret key to your {".env"}{" "} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx index d364d7ce1..74c677256 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx @@ -9,7 +9,7 @@ const installCodeBun = `bun add autumn-js`; export const Install = () => { return (
- + { ${betterAuthSnippet(customerType, "args.request.headers", 2)} }, @@ -24,7 +23,6 @@ import { autumnHandler } from "autumn-js/react-router"; import { createClient } from "@/utils/supabase/server"; const handler = autumnHandler({ - secretKey: process.env.AUTUMN_SECRET_KEY!, identify: async (args) => { ${supabaseAuthSnippet({ customerType })} }, @@ -62,7 +60,6 @@ import { autumnHandler } from "autumn-js/react-router"; import { getAuth } from "@clerk/react-router/ssr.server"; const handler = autumnHandler({ - secretKey: process.env.AUTUMN_SECRET_KEY!, identify: async (args) => { ${clerkSnippet(customerType)} }, @@ -77,7 +74,6 @@ export const rr7Other = (customerType: "user" | "org") => { import { autumnHandler } from "autumn-js/react-router"; const handler = autumnHandler({ - secretKey: process.env.AUTUMN_SECRET_KEY!, identify: async (args) => { const customerId = "your_customer_id"; // Get customer id from your database diff --git a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx index 9f1320507..dd50bac91 100644 --- a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx +++ b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx @@ -23,22 +23,21 @@ import { updateProduct } from "@/views/products/product/utils/updateProduct"; import { getBackendErr } from "@/utils/genUtils"; import { EditProductDetails } from "./edit-product/EditProductDetails"; -export const EditProduct = ({ data, mutate }: { data: any; mutate: any }) => { +export const EditProduct = ({ mutate }: { mutate: any }) => { const [freeTrialModalOpen, setFreeTrialModalOpen] = useState(false); - const { productCount, productDataState, mutateCounts } = + const { data, productCount, productDataState, mutateCounts } = useModelPricingContext(); const [showNewVersionDialog, setShowNewVersionDialog] = useState(false); + const { product, setProduct, - hasChanges, features, setFeatures, entityFeatureIds, setEntityFeatureIds, actionState, - isNewProduct, } = productDataState; const [details, setDetails] = useState({ @@ -51,15 +50,8 @@ export const EditProduct = ({ data, mutate }: { data: any; mutate: any }) => { const [saveLoading, setSaveLoading] = useState(false); - // useEffect(() => { - // if (data) { - // setFeatures(data.features); - // } - // }, [data]); - const runUpdateProduct = async () => { setSaveLoading(true); - // await handleCreateProduct(false); try { await updateProduct({ axiosInstance, @@ -85,6 +77,9 @@ export const EditProduct = ({ data, mutate }: { data: any; mutate: any }) => { const hasItems = product.items.length > 0; const hasCustomers = productCount?.all > 0; const showSaveButton = hasCustomers || product.version > 1; + const firstProductCreated = data.products.length > 0; + + const autoSave = !showSaveButton && firstProductCreated; const handleToggleSettings = async (key: string) => { if (!product) return; @@ -138,6 +133,11 @@ export const EditProduct = ({ data, mutate }: { data: any; mutate: any }) => { setOpen={setShowNewVersionDialog} createProduct={runUpdateProduct} /> + +
{ )}
- -
- -
- + + {firstProductCreated && ( + <> + {product.items.length == 0 ? ( +

+ Next, add items to define what customers with this product + get access to, and how much they should be charged for it. +

+ ) : ( +
+ +
+ )} + {" "} + + )}
{ const getCurProduct = () => { + console.log("data.products:", data.products); if (queryStates.productId) { const prod = data.products.find( (p: Product) => p.id === queryStates.productId @@ -62,21 +64,32 @@ export const ModelPricing = ({ if (prod) { return prod; } - } else if (data.products.length > 0) { + } + + if (data.products.length > 0) { + console.log("Returning first product:", data.products[0]); return data.products[0]; } + return defaultProduct; }; const curProduct = getCurProduct(); - const [firstItemCreated, setFirstItemCreated] = useState( - autumnProducts.some((p: Product) => p.items.length > 0) + // // console.log("curProduct:", curProduct); + // const [firstItemCreated, setFirstItemCreated] = useState( + // autumnProducts.some((p: Product) => p.items.length > 0) + // ); + + const firstItemCreated = autumnProducts.some( + (p: Product) => p.items.length > 0 ); const [editingNewProduct, setEditingNewProduct] = useState( nullish(curProduct) ); + const [connectStripeOpen, setConnectStripeOpen] = useState(false); + const productDataState = useProductData({ originalProduct: curProduct as any, originalFeatures: data.features as any, @@ -84,20 +97,6 @@ export const ModelPricing = ({ const { product } = productDataState; - // useEffect(() => { - // if (data) { - // const curProduct = data.products.find( - // (p: Product) => p.id === product.id - // ); - - // if (!curProduct) { - // if (data.products.length > 0) { - // setProduct(data.products[0]); - // } - // } - // } - // }, [data]); - if (!product) return null; const stripeConnected = data?.org.stripe_connected; @@ -106,7 +105,7 @@ export const ModelPricing = ({ + -
+
-
-

Create your plans

+
+
+

Create your products

+

+ To start, model your app's pricing by creating a product for + your free plans,
paid plans and any add-ons or + top-ups. +

+
{firstItemCreated && (
- + {data.products.length > 0 && }
)}
- +
-
- +
-
- {!stripeConnected && ( - - )} - -
- + +
+
+
+ +
+
- +
diff --git a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx index aebee5282..2e79ddb13 100644 --- a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx +++ b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx @@ -5,14 +5,23 @@ import { handleAutoSave } from "../model-pricing-utils/modelPricingUtils"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { useModelPricingContext } from "../ModelPricingContext"; import { slugify } from "@/utils/formatUtils/formatTextUtils"; +import { Button } from "@/components/ui/button"; +import { useProductContext } from "@/views/products/product/ProductContext"; +import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; +import { getBackendErr } from "@/utils/genUtils"; +import { toast } from "sonner"; export const EditProductDetails = () => { const { - editingNewProduct, productDataState: { product, setProduct }, + data, mutate, } = useModelPricingContext(); + const allowCreate = data.products.length === 0; + const { autoSave } = useProductContext(); + const [createLoading, setCreateLoading] = useState(false); + const axiosInstance = useAxiosInstance(); const [details, setDetails] = useState({ name: product?.name, @@ -20,6 +29,7 @@ export const EditProductDetails = () => { }); useEffect(() => { + console.log("product:", product); if (product.id) { setDetails({ name: product.name, @@ -28,22 +38,39 @@ export const EditProductDetails = () => { } }, [product]); + const handleCreateProduct = async () => { + setCreateLoading(true); + try { + await axiosInstance.post("/v1/products", { + name: details.name, + id: details.id, + }); + await mutate(); + } catch (error) { + toast.error(getBackendErr(error, "Failed to create product")); + } finally { + setCreateLoading(false); + } + }; + return ( -
+
- Name + Product Name { - await handleAutoSave({ - axiosInstance, - productId: product.id ? product.id : details.id, - product: { - ...product, - name: details.name, - id: details.id, - }, - mutate, - }); + if (autoSave && !allowCreate) { + await handleAutoSave({ + axiosInstance, + productId: product.id ? product.id : details.id, + product: { + ...product, + name: details.name, + id: details.id, + }, + mutate, + }); + } setProduct({ ...product, name: details.name, @@ -53,7 +80,7 @@ export const EditProductDetails = () => { placeholder="Eg. Free Plan" value={details.name} onChange={(e) => { - const newIdData = editingNewProduct + const newIdData = allowCreate ? { id: slugify(e.target.value), } @@ -67,9 +94,35 @@ export const EditProductDetails = () => { />
- ID - +
+ + Product ID + + +

+ The product ID is used to identify the product in the API when + you're making a payment. +

+
+
+ + { + setDetails({ + ...details, + id: e.target.value, + }); + }} + placeholder="Eg. free_plan" + />
+ {allowCreate && ( + + )}
); }; diff --git a/vite/src/views/onboarding2/utils/connectStripe.ts b/vite/src/views/onboarding2/utils/connectStripe.ts new file mode 100644 index 000000000..19b68f3b3 --- /dev/null +++ b/vite/src/views/onboarding2/utils/connectStripe.ts @@ -0,0 +1,28 @@ +import { OrgService } from "@/services/OrgService"; +import { getBackendErr } from "@/utils/genUtils"; +import { AxiosInstance } from "axios"; +import { toast } from "sonner"; + +export const connectStripe = async ({ + testApiKey, + axiosInstance, + mutate, +}: { + testApiKey: string; + axiosInstance: AxiosInstance; + mutate: () => void; +}) => { + try { + await OrgService.connectStripe(axiosInstance, { + testApiKey, + liveApiKey: testApiKey, + successUrl: `https://useautumn.com`, + }); + + toast.success("Successfully connected to Stripe"); + await mutate(); + } catch (error) { + console.log("Failed to connect Stripe", error); + toast.error(getBackendErr(error, "Failed to connect Stripe")); + } +}; diff --git a/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx b/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx index 377e378a8..212b8cea7 100644 --- a/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx +++ b/vite/src/views/products/product/free-trial/CreateFreeTrial.tsx @@ -20,11 +20,8 @@ export const CreateFreeTrial = ({ open: boolean; setOpen: (open: boolean) => void; }) => { - // const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const [price, setPrice] = useState(null); - const { env, product, setProduct, prices, autoSave, mutate } = - useProductContext(); + const { product, setProduct, autoSave, mutate } = useProductContext(); const axiosInstance = useAxiosInstance(); diff --git a/vite/src/views/products/product/product-item/CreateProductItem2.tsx b/vite/src/views/products/product/product-item/CreateProductItem2.tsx index 2fda08df5..8e102579d 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem2.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem2.tsx @@ -17,6 +17,7 @@ import { CreateItemDialogContent } from "./create-product-item/CreateItemDialogC import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelPricingContext"; import { useSteps } from "./useSteps"; import { CreateItemStep } from "./utils/CreateItemStep"; +import { cn } from "@/lib/utils"; const defaultProductItem: ProductItem = { feature_id: null, @@ -35,7 +36,13 @@ const defaultProductItem: ProductItem = { reset_usage_when_enabled: true, }; -export function CreateProductItem2() { +export function CreateProductItem2({ + classNames, +}: { + classNames?: { + button?: string; + }; +}) { const [open, setOpen] = useState(false); const [showCreateFeature, setShowCreateFeature] = useState(false); const [item, setItem] = useState(defaultProductItem); @@ -68,7 +75,7 @@ export function CreateProductItem2() { }, 400); setOpen(false); - setFirstItemCreated(true); + // setFirstItemCreated(true); return newProduct; }; @@ -93,7 +100,7 @@ export function CreateProductItem2() {
diff --git a/vite/src/views/products/product/product-item/ProductItemTable.tsx b/vite/src/views/products/product/product-item/ProductItemTable.tsx index 42c94a6de..546ece242 100644 --- a/vite/src/views/products/product/product-item/ProductItemTable.tsx +++ b/vite/src/views/products/product/product-item/ProductItemTable.tsx @@ -68,46 +68,57 @@ export const ProductItemTable = () => {
- {!isOnboarding && } - - -
*/}
From c5e83e9a738198c971cc29824a6e839c920c91ba Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 5 Aug 2025 08:28:40 -0700 Subject: [PATCH 12/37] test: pricing-table in onboarding --- vite/src/components/autumn/pricing-table.tsx | 7 ++++++- vite/src/views/onboarding2/OnboardingView2.tsx | 1 + vite/src/views/onboarding2/model-pricing/ModelPricing.tsx | 2 -- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/vite/src/components/autumn/pricing-table.tsx b/vite/src/components/autumn/pricing-table.tsx index ab4c33d7d..84a331bcc 100644 --- a/vite/src/components/autumn/pricing-table.tsx +++ b/vite/src/components/autumn/pricing-table.tsx @@ -70,18 +70,23 @@ export default function PricingTable({ product.scenario === "scheduled", onClick: async () => { + console.log("Inside onClick function"); if (!stripeConnected) { setConnectStripeOpen(true); return; } + console.log("Stripe connected"); + console.log("Product ID:", product.id); + if (product.id) { - await checkout({ + const result = await checkout({ productId: product.id, dialog: CheckoutDialog, openInNewTab: true, successUrl: `${window.location.origin}`, }); + console.log("Result:", result); } else if (product.display?.button_url) { window.open(product.display?.button_url, "_blank"); } diff --git a/vite/src/views/onboarding2/OnboardingView2.tsx b/vite/src/views/onboarding2/OnboardingView2.tsx index 6ed1b3b4f..e8e76ccb7 100644 --- a/vite/src/views/onboarding2/OnboardingView2.tsx +++ b/vite/src/views/onboarding2/OnboardingView2.tsx @@ -57,6 +57,7 @@ export default function OnboardingView2() { }); await productMutate(); + await mutateAutumnProducts(); } catch (error) { console.error(error); } finally { diff --git a/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx b/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx index 2f8ad12a7..2ae8733b2 100644 --- a/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx +++ b/vite/src/views/onboarding2/model-pricing/ModelPricing.tsx @@ -56,7 +56,6 @@ export const ModelPricing = ({ setQueryStates: any; }) => { const getCurProduct = () => { - console.log("data.products:", data.products); if (queryStates.productId) { const prod = data.products.find( (p: Product) => p.id === queryStates.productId @@ -67,7 +66,6 @@ export const ModelPricing = ({ } if (data.products.length > 0) { - console.log("Returning first product:", data.products[0]); return data.products[0]; } From 16ff4ddf91881f5f197b47cf802f10e28df8560d Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 5 Aug 2025 08:32:56 -0700 Subject: [PATCH 13/37] fix: getEntityUtils when customer isn't found --- server/src/internal/api/entities/getEntityUtils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts index 595d75ed0..1834433f6 100644 --- a/server/src/internal/api/entities/getEntityUtils.ts +++ b/server/src/internal/api/entities/getEntityUtils.ts @@ -72,6 +72,14 @@ export const getEntityResponse = async ({ skipCache, }); + if (!customer) { + throw new RecaseError({ + message: `Customer ${customerId} not found`, + code: ErrCode.CustomerNotFound, + statusCode: 400, + }); + } + let entities = customer.entities.filter((e: Entity) => entityIds.includes(e.id) ); From 88ea74ab045f5df36d0a5989ae4e63bfd53708b5 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 5 Aug 2025 11:33:31 -0700 Subject: [PATCH 14/37] fix: changed add product item text --- bun.lock | 571 ++++++++++++++++++ .../product-item/CreateProductItem2.tsx | 2 +- .../CreateFeatureFromItem.tsx | 101 ---- 3 files changed, 572 insertions(+), 102 deletions(-) delete mode 100644 vite/src/views/products/product/product-item/create-product-item/CreateFeatureFromItem.tsx diff --git a/bun.lock b/bun.lock index 2ad733296..696dc89e0 100644 --- a/bun.lock +++ b/bun.lock @@ -87,6 +87,7 @@ "resend": "^4.1.1", "stripe": "^17.5.0", "svix": "^1.45.1", + "traceroot-sdk-ts": "0.0.1-alpha.4", "tsc-alias": "^1.8.16", "ws": "^8.18.0", "zod": "^3.25.23", @@ -245,6 +246,64 @@ "@autumn/vite": ["@autumn/vite@workspace:vite"], + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.859.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.858.0", "@aws-sdk/credential-provider-node": "3.859.0", "@aws-sdk/middleware-host-header": "3.840.0", "@aws-sdk/middleware-logger": "3.840.0", "@aws-sdk/middleware-recursion-detection": "3.840.0", "@aws-sdk/middleware-user-agent": "3.858.0", "@aws-sdk/region-config-resolver": "3.840.0", "@aws-sdk/types": "3.840.0", "@aws-sdk/util-endpoints": "3.848.0", "@aws-sdk/util-user-agent-browser": "3.840.0", "@aws-sdk/util-user-agent-node": "3.858.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.7.2", "@smithy/eventstream-serde-browser": "^4.0.4", "@smithy/eventstream-serde-config-resolver": "^4.1.2", "@smithy/eventstream-serde-node": "^4.0.4", "@smithy/fetch-http-handler": "^5.1.0", "@smithy/hash-node": "^4.0.4", "@smithy/invalid-dependency": "^4.0.4", "@smithy/middleware-content-length": "^4.0.4", "@smithy/middleware-endpoint": "^4.1.17", "@smithy/middleware-retry": "^4.1.18", "@smithy/middleware-serde": "^4.0.8", "@smithy/middleware-stack": "^4.0.4", "@smithy/node-config-provider": "^4.1.3", "@smithy/node-http-handler": "^4.1.0", "@smithy/protocol-http": "^5.1.2", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "@smithy/url-parser": "^4.0.4", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.25", "@smithy/util-defaults-mode-node": "^4.0.25", "@smithy/util-endpoints": "^3.0.6", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", "@smithy/util-utf8": "^4.0.0", "@types/uuid": "^9.0.1", "tslib": "^2.6.2", "uuid": "^9.0.1" } }, "sha512-JwBT76jTRVOJLwxw6T9jxj/9jQH1yf1aaojilwtYUX1nudA2tbQWuDFJz26YnyoTIufPjyFg8cekmmmb6Jh7TA=="], + + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.858.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.858.0", "@aws-sdk/middleware-host-header": "3.840.0", "@aws-sdk/middleware-logger": "3.840.0", "@aws-sdk/middleware-recursion-detection": "3.840.0", "@aws-sdk/middleware-user-agent": "3.858.0", "@aws-sdk/region-config-resolver": "3.840.0", "@aws-sdk/types": "3.840.0", "@aws-sdk/util-endpoints": "3.848.0", "@aws-sdk/util-user-agent-browser": "3.840.0", "@aws-sdk/util-user-agent-node": "3.858.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.7.2", "@smithy/fetch-http-handler": "^5.1.0", "@smithy/hash-node": "^4.0.4", "@smithy/invalid-dependency": "^4.0.4", "@smithy/middleware-content-length": "^4.0.4", "@smithy/middleware-endpoint": "^4.1.17", "@smithy/middleware-retry": "^4.1.18", "@smithy/middleware-serde": "^4.0.8", "@smithy/middleware-stack": "^4.0.4", "@smithy/node-config-provider": "^4.1.3", "@smithy/node-http-handler": "^4.1.0", "@smithy/protocol-http": "^5.1.2", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "@smithy/url-parser": "^4.0.4", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.25", "@smithy/util-defaults-mode-node": "^4.0.25", "@smithy/util-endpoints": "^3.0.6", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-iXuZQs4KH6a3Pwnt0uORalzAZ5EXRPr3lBYAsdNwkP8OYyoUz5/TE3BLyw7ceEh0rj4QKGNnNALYo1cDm0EV8w=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.858.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@aws-sdk/xml-builder": "3.821.0", "@smithy/core": "^3.7.2", "@smithy/node-config-provider": "^4.1.3", "@smithy/property-provider": "^4.0.4", "@smithy/protocol-http": "^5.1.2", "@smithy/signature-v4": "^5.1.2", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.4", "@smithy/util-utf8": "^4.0.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-iWm4QLAS+/XMlnecIU1Y33qbBr1Ju+pmWam3xVCPlY4CSptKpVY+2hXOnmg9SbHAX9C005fWhrIn51oDd00c9A=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.858.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-kZsGyh2BoSRguzlcGtzdLhw/l/n3KYAC+/l/H0SlsOq3RLHF6tO/cRdsLnwoix2bObChHUp03cex63o1gzdx/Q=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.858.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/fetch-http-handler": "^5.1.0", "@smithy/node-http-handler": "^4.1.0", "@smithy/property-provider": "^4.0.4", "@smithy/protocol-http": "^5.1.2", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-GDnfYl3+NPJQ7WQQYOXEA489B212NinpcIDD7rpsB6IWUPo8yDjT5NceK4uUkIR3MFpNCGt9zd/z6NNLdB2fuQ=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.859.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/credential-provider-env": "3.858.0", "@aws-sdk/credential-provider-http": "3.858.0", "@aws-sdk/credential-provider-process": "3.858.0", "@aws-sdk/credential-provider-sso": "3.859.0", "@aws-sdk/credential-provider-web-identity": "3.858.0", "@aws-sdk/nested-clients": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-KsccE1T88ZDNhsABnqbQj014n5JMDilAroUErFbGqu5/B3sXqUsYmG54C/BjvGTRUFfzyttK9lB9P9h6ddQ8Cw=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.859.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.858.0", "@aws-sdk/credential-provider-http": "3.858.0", "@aws-sdk/credential-provider-ini": "3.859.0", "@aws-sdk/credential-provider-process": "3.858.0", "@aws-sdk/credential-provider-sso": "3.859.0", "@aws-sdk/credential-provider-web-identity": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-ZRDB2xU5aSyTR/jDcli30tlycu6RFvQngkZhBs9Zoh2BiYXrfh2MMuoYuZk+7uD6D53Q2RIEldDHR9A/TPlRuA=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.858.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-l5LJWZJMRaZ+LhDjtupFUKEC5hAjgvCRrOvV5T60NCUBOy0Ozxa7Sgx3x+EOwiruuoh3Cn9O+RlbQlJX6IfZIw=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.859.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.858.0", "@aws-sdk/core": "3.858.0", "@aws-sdk/token-providers": "3.859.0", "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-BwAqmWIivhox5YlFRjManFF8GoTvEySPk6vsJNxDsmGsabY+OQovYxFIYxRCYiHzH7SFjd4Lcd+riJOiXNsvRw=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.858.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/nested-clients": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-8iULWsH83iZDdUuiDsRb83M0NqIlXjlDbJUIddVsIrfWp4NmanKw77SV6yOZ66nuJjPsn9j7RDb9bfEPCy5SWA=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.840.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.840.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.840.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.858.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/types": "3.840.0", "@aws-sdk/util-endpoints": "3.848.0", "@smithy/core": "^3.7.2", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-pC3FT/sRZ6n5NyXiTVu9dpf1D9j3YbJz3XmeOOwJqO/Mib2PZyIQktvNMPgwaC5KMVB1zWqS5bmCwxpMOnq0UQ=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.858.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.858.0", "@aws-sdk/middleware-host-header": "3.840.0", "@aws-sdk/middleware-logger": "3.840.0", "@aws-sdk/middleware-recursion-detection": "3.840.0", "@aws-sdk/middleware-user-agent": "3.858.0", "@aws-sdk/region-config-resolver": "3.840.0", "@aws-sdk/types": "3.840.0", "@aws-sdk/util-endpoints": "3.848.0", "@aws-sdk/util-user-agent-browser": "3.840.0", "@aws-sdk/util-user-agent-node": "3.858.0", "@smithy/config-resolver": "^4.1.4", "@smithy/core": "^3.7.2", "@smithy/fetch-http-handler": "^5.1.0", "@smithy/hash-node": "^4.0.4", "@smithy/invalid-dependency": "^4.0.4", "@smithy/middleware-content-length": "^4.0.4", "@smithy/middleware-endpoint": "^4.1.17", "@smithy/middleware-retry": "^4.1.18", "@smithy/middleware-serde": "^4.0.8", "@smithy/middleware-stack": "^4.0.4", "@smithy/node-config-provider": "^4.1.3", "@smithy/node-http-handler": "^4.1.0", "@smithy/protocol-http": "^5.1.2", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "@smithy/url-parser": "^4.0.4", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.25", "@smithy/util-defaults-mode-node": "^4.0.25", "@smithy/util-endpoints": "^3.0.6", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-ChdIj80T2whoWbovmO7o8ICmhEB2S9q4Jes9MBnKAPm69PexcJAK2dQC8yI4/iUP8b3+BHZoUPrYLWjBxIProQ=="], + + "@aws-sdk/node-http-handler": ["@aws-sdk/node-http-handler@3.374.0", "", { "dependencies": { "@smithy/node-http-handler": "^1.0.2", "tslib": "^2.5.0" } }, "sha512-v1Z6m0wwkf65/tKuhwrtPRqVoOtNkDTRn2MBMtxCwEw+8V8Q+YRFqVgGN+J1n53ktE0G5OYVBux/NHiAjJHReQ=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.840.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" } }, "sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.859.0", "", { "dependencies": { "@aws-sdk/core": "3.858.0", "@aws-sdk/nested-clients": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-6P2wlvm9KBWOvRNn0Pt8RntnXg8fzOb5kEShvWsOsAocZeqKNaYbihum5/Onq1ZPoVtkdb++8eWDocDnM4k85Q=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.840.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.848.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "@smithy/url-parser": "^4.0.4", "@smithy/util-endpoints": "^3.0.6", "tslib": "^2.6.2" } }, "sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.804.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.840.0", "", { "dependencies": { "@aws-sdk/types": "3.840.0", "@smithy/types": "^4.3.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.858.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.858.0", "@aws-sdk/types": "3.840.0", "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-T1m05QlN8hFpx5/5duMjS8uFSK5e6EXP45HQRkZULVkL3DK+jMaxsnh3KLl5LjUoHn/19M4HM0wNUBhYp4Y2Yw=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.821.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA=="], + "@axiomhq/js": ["@axiomhq/js@1.3.1", "", { "dependencies": { "fetch-retry": "^6.0.0", "uuid": "^11.0.2" } }, "sha512-Ytf5V3wKz8FKNiqJxnqZmUhjgJ7TItKUoyHVNE/H2V9dN1ozD6NNnsueenOjKdA48cm2sGRyP432nworst18aA=="], "@axiomhq/pino": ["@axiomhq/pino@1.3.1", "", { "dependencies": { "@axiomhq/js": "1.3.1", "pino-abstract-transport": "^1.2.0" } }, "sha512-zf6p2rU+b5XAk8Nj6EdjqdXTCuWQlf+C8UGdumD9xbtDWBYvk/EYkxXKMqK8mo2Gp+Fi+p5eHgjuOtbeop2XBQ=="], @@ -331,6 +390,8 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + "@date-fns/tz": ["@date-fns/tz@1.3.1", "", {}, "sha512-LnBOyuj+piItX/D5BWBSckBsuZyOt7Jg2obGNiObq7qjl1A2/8F+i4RS8/MmkSdnw6hOe6afrJLCWrUWZw5Mlw=="], "@date-fns/utc": ["@date-fns/utc@2.1.1", "", {}, "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="], @@ -977,6 +1038,94 @@ "@simplewebauthn/server": ["@simplewebauthn/server@13.1.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g=="], + "@smithy/abort-controller": ["@smithy/abort-controller@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.1.4", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" } }, "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w=="], + + "@smithy/core": ["@smithy/core@3.7.2", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.8", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.4", "@smithy/util-stream": "^4.2.3", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.0.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.3", "@smithy/property-provider": "^4.0.4", "@smithy/types": "^4.3.1", "@smithy/url-parser": "^4.0.4", "tslib": "^2.6.2" } }, "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.0.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.0.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.1.2", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.0.4", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.0.4", "", { "dependencies": { "@smithy/eventstream-codec": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.1.0", "", { "dependencies": { "@smithy/protocol-http": "^5.1.2", "@smithy/querystring-builder": "^4.0.4", "@smithy/types": "^4.3.1", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.0.4", "", { "dependencies": { "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.1.17", "", { "dependencies": { "@smithy/core": "^3.7.2", "@smithy/middleware-serde": "^4.0.8", "@smithy/node-config-provider": "^4.1.3", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "@smithy/url-parser": "^4.0.4", "@smithy/util-middleware": "^4.0.4", "tslib": "^2.6.2" } }, "sha512-S3hSGLKmHG1m35p/MObQCBCdRsrpbPU8B129BVzRqRfDvQqPMQ14iO4LyRw+7LNizYc605COYAcjqgawqi+6jA=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.1.18", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.3", "@smithy/protocol-http": "^5.1.2", "@smithy/service-error-classification": "^4.0.6", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", "tslib": "^2.6.2", "uuid": "^9.0.1" } }, "sha512-bYLZ4DkoxSsPxpdmeapvAKy7rM5+25gR7PGxq2iMiecmbrRGBHj9s75N74Ylg+aBiw9i5jIowC/cLU2NR0qH8w=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.8", "", { "dependencies": { "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.1.3", "", { "dependencies": { "@smithy/property-provider": "^4.0.4", "@smithy/shared-ini-file-loader": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.1.0", "", { "dependencies": { "@smithy/abort-controller": "^4.0.4", "@smithy/protocol-http": "^5.1.2", "@smithy/querystring-builder": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.1.2", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.0.6", "", { "dependencies": { "@smithy/types": "^4.3.1" } }, "sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.1.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-middleware": "^4.0.4", "@smithy/util-uri-escape": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.4.9", "", { "dependencies": { "@smithy/core": "^3.7.2", "@smithy/middleware-endpoint": "^4.1.17", "@smithy/middleware-stack": "^4.0.4", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" } }, "sha512-mbMg8mIUAWwMmb74LoYiArP04zWElPzDoA1jVOp3or0cjlDMgoS6WTC3QXK0Vxoc9I4zdrX0tq6qsOmaIoTWEQ=="], + + "@smithy/types": ["@smithy/types@4.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.0.4", "", { "dependencies": { "@smithy/querystring-parser": "^4.0.4", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.0.25", "", { "dependencies": { "@smithy/property-provider": "^4.0.4", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-pxEWsxIsOPLfKNXvpgFHBGFC3pKYKUFhrud1kyooO9CJai6aaKDHfT10Mi5iiipPXN/JhKAu3qX9o75+X85OdQ=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.0.25", "", { "dependencies": { "@smithy/config-resolver": "^4.1.4", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/node-config-provider": "^4.1.3", "@smithy/property-provider": "^4.0.4", "@smithy/smithy-client": "^4.4.9", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.0.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.3", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.0.6", "", { "dependencies": { "@smithy/service-error-classification": "^4.0.6", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.2.3", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.1.0", "@smithy/node-http-handler": "^4.1.0", "@smithy/types": "^4.3.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="], + "@socket.io/component-emitter": ["@socket.io/component-emitter@3.1.2", "", {}, "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="], "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], @@ -1037,6 +1186,8 @@ "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], + "@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], "@tsconfig/node10": ["@tsconfig/node10@1.0.11", "", {}, "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw=="], @@ -1177,6 +1328,8 @@ "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], @@ -1285,6 +1438,8 @@ "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-listen": ["async-listen@3.1.0", "", {}, "sha512-TkOhqze98lP+6e7SPbrBpyhTpfvqqX8VYKGn4uckrgPan4WQIHnTaUD2zZzZS18eVVDj4rHPcIZa1PGgvo1DfA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -1331,6 +1486,8 @@ "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + "bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="], + "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -1421,6 +1578,8 @@ "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], @@ -1449,6 +1608,8 @@ "core-js": ["core-js@3.45.0", "", {}, "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA=="], + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], "create-require": ["create-require@1.1.1", "", {}, "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="], @@ -1579,6 +1740,8 @@ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -1673,6 +1836,8 @@ "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], @@ -1691,6 +1856,8 @@ "file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="], + "file-uri-to-path": ["file-uri-to-path@2.0.0", "", {}, "sha512-hjPFI8oE/2iQPVe4gbrJ73Pp+Xfub2+WI2LlXDbsaJBwT5wuMh35WNWVYYTpnz895shtwfyutMFLFywpQAFdLg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], @@ -1703,6 +1870,8 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -1723,8 +1892,12 @@ "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "ftp": ["ftp@0.3.10", "", { "dependencies": { "readable-stream": "1.1.x", "xregexp": "2.0.0" } }, "sha512-faFVML1aBx2UoDStmLwv2Wptt4vw5x03xxX172nhA5Y5HBshW5JweqQ2W4xL4dezQTG8inJsuYcpPHHU3X5OTQ=="], + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-timeout": ["function-timeout@0.1.1", "", {}, "sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg=="], @@ -1839,6 +2012,8 @@ "ioredis": ["ioredis@5.7.0", "", { "dependencies": { "@ioredis/commands": "^1.3.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g=="], + "ip": ["ip@1.1.9", "", {}, "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ=="], + "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], "ip-regex": ["ip-regex@5.0.0", "", {}, "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw=="], @@ -1877,6 +2052,8 @@ "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "isarray": ["isarray@0.0.1", "", {}, "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], @@ -1915,6 +2092,8 @@ "jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="], + "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + "jss": ["jss@10.10.0", "", { "dependencies": { "@babel/runtime": "^7.3.1", "csstype": "^3.0.2", "is-in-browser": "^1.1.3", "tiny-warning": "^1.0.2" } }, "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw=="], "jss-plugin-camel-case": ["jss-plugin-camel-case@10.10.0", "", { "dependencies": { "@babel/runtime": "^7.3.1", "hyphenate-style-name": "^1.0.3", "jss": "10.10.0" } }, "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw=="], @@ -1947,6 +2126,8 @@ "ksuid": ["ksuid@3.0.0", "", { "dependencies": { "base-convert-int-array": "^1.0.1" } }, "sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "kysely": ["kysely@0.28.4", "", {}, "sha512-pfQj8/Bo3KSzC1HIZB5MeeYRWcDmx1ZZv8H25LsyeygqXE+gfsbUAgPT1GSYZFctB1cdOVlv+OifuCls2mQSnw=="], "leac": ["leac@0.6.0", "", {}, "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg=="], @@ -2129,6 +2310,8 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], @@ -2477,6 +2660,8 @@ "stripe": ["stripe@17.7.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw=="], + "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "super-regex": ["super-regex@0.2.0", "", { "dependencies": { "clone-regexp": "^3.0.0", "function-timeout": "^0.1.0", "time-span": "^5.1.0" } }, "sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw=="], @@ -2515,6 +2700,8 @@ "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "theming": ["theming@3.3.0", "", { "dependencies": { "hoist-non-react-statics": "^3.3.0", "prop-types": "^15.5.8", "react-display-name": "^0.2.4", "tiny-warning": "^1.0.2" }, "peerDependencies": { "react": ">=16.3" } }, "sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA=="], "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], @@ -2539,6 +2726,8 @@ "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + "traceroot-sdk-ts": ["traceroot-sdk-ts@0.0.1-alpha.4", "", { "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.855.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.53.0", "@opentelemetry/exporter-trace-otlp-http": "^0.53.0", "@opentelemetry/resources": "^1.28.0", "@opentelemetry/sdk-node": "^0.53.0", "@opentelemetry/sdk-trace-base": "^1.28.0", "@opentelemetry/sdk-trace-node": "^1.28.0", "@opentelemetry/semantic-conventions": "^1.36.0", "axios": "^1.6.0", "winston": "^3.11.0", "winston-cloudwatch-logs": "^0.1.2", "yaml": "^2.3.4" } }, "sha512-f4I3yUUgvlBsUArSXVWC9HS062TXyK+mISfMA5j6clugDnuXe8n96AH8fA+Zw2qiyYgYL/rpCbjr7gJ0A90MZA=="], + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -2585,6 +2774,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], @@ -2619,6 +2810,8 @@ "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + "vm2": ["vm2@3.9.19", "", { "dependencies": { "acorn": "^8.7.0", "acorn-walk": "^8.2.0" }, "bin": { "vm2": "bin/vm2" } }, "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg=="], + "vscode-oniguruma": ["vscode-oniguruma@2.0.1", "", {}, "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ=="], "vscode-textmate": ["vscode-textmate@9.2.0", "", {}, "sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA=="], @@ -2639,6 +2832,10 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-cloudwatch-logs": ["winston-cloudwatch-logs@0.1.2", "", { "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.47.0", "@aws-sdk/node-http-handler": "^3.47.0", "fast-safe-stringify": "^2.1.1", "proxy-agent": "^5.0.0", "winston-transport": "^4.4.2" }, "peerDependencies": { "winston": "^3.4.0" } }, "sha512-DvdQr0H1Z7dNJ9e3gmw12yNxFd9hF0V54ogN/3jxhRyM/wGFiSAmRV+yJI9XeBDFiGtrcxIqywjnyXceU3h1CQ=="], + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -2653,12 +2850,16 @@ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "xregexp": ["xregexp@2.0.0", "", {}, "sha512-xl/50/Cf32VsGq/1R8jJE5ajH1yMCQkpmoS10QbFZWl2Oor4H0Me64Pu2yxvsRWK3m6soJbmGfzSR7BYmDcWAA=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], @@ -2699,6 +2900,12 @@ "@autumn/vite/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-sdk/node-http-handler/@smithy/node-http-handler": ["@smithy/node-http-handler@1.1.0", "", { "dependencies": { "@smithy/abort-controller": "^1.1.0", "@smithy/protocol-http": "^1.2.0", "@smithy/querystring-builder": "^1.1.0", "@smithy/types": "^1.2.0", "tslib": "^2.5.0" } }, "sha512-d3kRriEgaIiGXLziAM8bjnaLn1fthCJeTLZIwEIpzQqe6yPX0a+yQoLCTyjb2fvdLwkMoG4p7THIIB5cj5lkbg=="], + "@axiomhq/js/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -3059,6 +3266,8 @@ "cloudflare/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="], + "colorspace/color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + "concurrently/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "engine.io/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], @@ -3081,6 +3290,8 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ftp/readable-stream": ["readable-stream@1.1.14", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ=="], + "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -3155,6 +3366,18 @@ "svix/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node": ["@opentelemetry/auto-instrumentations-node@0.53.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/instrumentation-amqplib": "^0.44.0", "@opentelemetry/instrumentation-aws-lambda": "^0.48.0", "@opentelemetry/instrumentation-aws-sdk": "^0.47.0", "@opentelemetry/instrumentation-bunyan": "^0.43.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.43.0", "@opentelemetry/instrumentation-connect": "^0.41.0", "@opentelemetry/instrumentation-cucumber": "^0.11.0", "@opentelemetry/instrumentation-dataloader": "^0.14.0", "@opentelemetry/instrumentation-dns": "^0.41.0", "@opentelemetry/instrumentation-express": "^0.45.0", "@opentelemetry/instrumentation-fastify": "^0.42.0", "@opentelemetry/instrumentation-fs": "^0.17.0", "@opentelemetry/instrumentation-generic-pool": "^0.41.0", "@opentelemetry/instrumentation-graphql": "^0.45.0", "@opentelemetry/instrumentation-grpc": "^0.55.0", "@opentelemetry/instrumentation-hapi": "^0.43.0", "@opentelemetry/instrumentation-http": "^0.55.0", "@opentelemetry/instrumentation-ioredis": "^0.45.0", "@opentelemetry/instrumentation-kafkajs": "^0.5.0", "@opentelemetry/instrumentation-knex": "^0.42.0", "@opentelemetry/instrumentation-koa": "^0.45.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.42.0", "@opentelemetry/instrumentation-memcached": "^0.41.0", "@opentelemetry/instrumentation-mongodb": "^0.49.0", "@opentelemetry/instrumentation-mongoose": "^0.44.0", "@opentelemetry/instrumentation-mysql": "^0.43.0", "@opentelemetry/instrumentation-mysql2": "^0.43.0", "@opentelemetry/instrumentation-nestjs-core": "^0.42.0", "@opentelemetry/instrumentation-net": "^0.41.0", "@opentelemetry/instrumentation-pg": "^0.48.0", "@opentelemetry/instrumentation-pino": "^0.44.0", "@opentelemetry/instrumentation-redis": "^0.44.0", "@opentelemetry/instrumentation-redis-4": "^0.44.0", "@opentelemetry/instrumentation-restify": "^0.43.0", "@opentelemetry/instrumentation-router": "^0.42.0", "@opentelemetry/instrumentation-socket.io": "^0.44.0", "@opentelemetry/instrumentation-tedious": "^0.16.0", "@opentelemetry/instrumentation-undici": "^0.8.0", "@opentelemetry/instrumentation-winston": "^0.42.0", "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.5", "@opentelemetry/resource-detector-aws": "^1.8.0", "@opentelemetry/resource-detector-azure": "^0.3.0", "@opentelemetry/resource-detector-container": "^0.5.1", "@opentelemetry/resource-detector-gcp": "^0.30.0", "@opentelemetry/resources": "^1.24.0", "@opentelemetry/sdk-node": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.4.1" } }, "sha512-AI3VQX1L2g4Xya8fPE1aahVhvya8/ikU7o2kMbry122Gd4kDVph41pejdOhWa/oNUgPRC6FLJmx7SZZ6/ShVjQ=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-m7F5ZTq+V9mKGWYpX8EnZ7NjoqAU7VemQ1E2HAG+W/u0wpY1x0OmbxAXfGKFHCspdJk8UKlwPGrpcB8nay3P8A=="], + + "traceroot-sdk-ts/@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.53.0", "@opentelemetry/exporter-logs-otlp-http": "0.53.0", "@opentelemetry/exporter-logs-otlp-proto": "0.53.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.53.0", "@opentelemetry/exporter-trace-otlp-http": "0.53.0", "@opentelemetry/exporter-trace-otlp-proto": "0.53.0", "@opentelemetry/exporter-zipkin": "1.26.0", "@opentelemetry/instrumentation": "0.53.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "@opentelemetry/sdk-trace-node": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-0hsxfq3BKy05xGktwG8YdGdxV978++x40EAKyKr1CaHZRh8uqVlXnclnl7OMi9xLMJEcXUw7lGhiRlArFcovyg=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@1.30.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "1.30.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/propagator-b3": "1.30.1", "@opentelemetry/propagator-jaeger": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ=="], + "ts-node/diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], "tsc-alias/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -3163,6 +3386,10 @@ "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "winston/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "winston-cloudwatch-logs/proxy-agent": ["proxy-agent@5.0.0", "", { "dependencies": { "agent-base": "^6.0.0", "debug": "4", "http-proxy-agent": "^4.0.0", "https-proxy-agent": "^5.0.0", "lru-cache": "^5.1.1", "pac-proxy-agent": "^5.0.0", "proxy-from-env": "^1.0.0", "socks-proxy-agent": "^5.0.0" } }, "sha512-gkH7BkvLVkSfX9Dk27W6TyNOWWZWRilRfk1XxGNWOYJ2TuedAv1yFpCaU9QSBmBe716XOTNpYNOzhysyw8xn7g=="], + "winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -3177,6 +3404,18 @@ "@autumn/vite/stripe/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-sdk/node-http-handler/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@1.1.0", "", { "dependencies": { "@smithy/types": "^1.2.0", "tslib": "^2.5.0" } }, "sha512-5imgGUlZL4dW4YWdMYAKLmal9ny/tlenM81QZY7xYyb76z9Z/QOg7oM5Ak9HQl8QfFTlGVWwcMXl+54jroRgEQ=="], + + "@aws-sdk/node-http-handler/@smithy/node-http-handler/@smithy/protocol-http": ["@smithy/protocol-http@1.2.0", "", { "dependencies": { "@smithy/types": "^1.2.0", "tslib": "^2.5.0" } }, "sha512-GfGfruksi3nXdFok5RhgtOnWe5f6BndzYfmEXISD+5gAGdayFGpjWu5pIqIweTudMtse20bGbc+7MFZXT1Tb8Q=="], + + "@aws-sdk/node-http-handler/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@1.1.0", "", { "dependencies": { "@smithy/types": "^1.2.0", "@smithy/util-uri-escape": "^1.1.0", "tslib": "^2.5.0" } }, "sha512-gDEi4LxIGLbdfjrjiY45QNbuDmpkwh9DX4xzrR2AzjjXpxwGyfSpbJaYhXARw9p17VH0h9UewnNQXNwaQyYMDA=="], + + "@aws-sdk/node-http-handler/@smithy/node-http-handler/@smithy/types": ["@smithy/types@1.2.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@browserbasehq/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], @@ -3477,6 +3716,8 @@ "cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "colorspace/color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -3489,6 +3730,8 @@ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ftp/readable-stream/string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="], + "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -3525,16 +3768,176 @@ "svix/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-YDCMlaQRZkziLL3t6TONRgmmGxDx6MyQDXRD0dknkkgUZtOK5+8MWft1OXzmNu6XfBOdT12MKN5rz+jHUkafKQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.44.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n2nn2jD1zWeKQOfmDTMXmypHJ2DmyTGZADOYLxRlYNDOv69lTPLZYaxVIUEdnCvioLSuVnB8zPzy077gEKcCaQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-aws-lambda": ["@opentelemetry/instrumentation-aws-lambda@0.48.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/aws-lambda": "8.10.143" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-0BJHjCUQwDO5uMCAE1C06LoXcLPK3lWlnT40AORFU9DvT/tFFCjs+KlN3vE39FSlWL7vVzyMVOejdcbDv+xMlw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-aws-sdk": ["@opentelemetry/instrumentation-aws-sdk@0.47.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/propagation-utils": "^0.30.13", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-taO5tsee7g5Q71LRebnHSDb8oIEcGDaqMol0gMJdPCAZAu4pZ7vixDGCONAvIo9OgrR948h/NhQX4T0cLJ1fag=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-bunyan": ["@opentelemetry/instrumentation-bunyan@0.43.0", "", { "dependencies": { "@opentelemetry/api-logs": "^0.55.0", "@opentelemetry/instrumentation": "^0.55.0", "@types/bunyan": "1.8.9" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nAAXMx63tXXWwuPiTLWTxDRBqXDRvcfE4H3IrXZbrls3BO7P7SkTZ9dvwPCuTku4rRUhEEDpV8vq9Ng4Pk/Uzw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-cassandra-driver": ["@opentelemetry/instrumentation-cassandra-driver@0.43.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fpnGDwUA5nRFhMDb4N1JBUi3dzsHvZRFcyX5bIXoApx43ZwY3lP/eF44aiHE6a4YObgcStLchLa0bEDM5UT4Fw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.41.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.36" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BFbkWYVzvSG9G9bG/8vp3+VWRfFgBqPPG0fQh4oM8nrz3YWrHK6269PIXmk9W5hXoxvYw0ghzp2kjMXIzX+NeA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-cucumber": ["@opentelemetry/instrumentation-cucumber@0.11.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-6CyeH678mw5AYbXIY1wtuNL7OsE57+XXk5t5pBeiXsAg0Kh0084/MmBzzCNVOCxn+IN5sjXKtjgVIDHrE/iILA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.14.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1cQC0CUSCDbyACFA8f8limjYyQbNdYdiKzGIJF2MwSUkhac64WvcoNjknYfK7CCO68QrBmvmaLqoF+IbZ7djZg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-dns": ["@opentelemetry/instrumentation-dns@0.41.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4SovC9rlhBcRzlAmw8PZD3tcP8CfIZ8GJIKJlB5Lca7IDh2A92JpOqzrWFCOJVGFYt7E6YeZJ09b+yb/4Ypa5Q=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.45.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7NY+HsETxEP5Rtlhy8Z3pPJdiz6wPmJuFVb9bRDdThKk72ATryox2ozV3t+aMeOdDsVgQiPHpgPzU150/uovOQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-fastify": ["@opentelemetry/instrumentation-fastify@0.42.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XmLaOI4rCqcuBwL+u/vh+hJdLCaZsjc7Q88BCtvLAQhnrj02UEX3c+MDRMcCAoxUJMQTSJMlCOv/tfibWdrVAg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.17.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-WKO2hBdU24LD4VlSNOIWRAP3JegTmDtZtoy0H92ipKeVajvlSMewozvTXiGd2+hF7WY3zL6/sbx47t6ycq9SrA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.41.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-V0OcN7VH37laZU1pxLixFROBkXrT55E5/MpacShsziAhGqiPZyU1XlCAHBseZ0T7cPfQ8Ux3cp0BAv59hRPt1Q=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.45.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-NCmL89XZcu9NQAskrYsUHT0PygUiLX90GwjS7kUn72nRAuk/myGg8Zj9YUPwe/OKVJcSLA5Fq755jUHlBQ1odA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-grpc": ["@opentelemetry/instrumentation-grpc@0.55.0", "", { "dependencies": { "@opentelemetry/instrumentation": "0.55.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-n2ZH4pRwOy0Vhag/3eKqiyDBwcpUnGgJI9iiIRX7vivE0FMncaLazWphNFezRRaM/LuKwq1TD8pVUvieP68mow=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.43.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FsSfn3nWNucswySEK/3EDV9vtgtj24YluVausqWMZiQlTlsLPzTbu2lUl7ynQViJGsUYh0YNpNz9d4IdzAGtcQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/instrumentation": "0.55.0", "@opentelemetry/semantic-conventions": "1.27.0", "forwarded-parse": "2.1.2", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-AO27XSjkgNicfy/YBthskFAwx9VfaO7tChrLaTONTfOWv14GlB3Rs2eTYpywZIHWsW2cR5hvVkcDte4GV0stoA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.45.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h79ctSTYgxc6V0saa4JcdjEt/JQd9gkfgFwPNyHZkIx0aQofygMc32Ulp2v7axAHqf8HiI9jP9aP/Qh1mWVSNA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.5.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-34Jv473IVv5uKFPz9m1ONX4DAnIxPXB5xKW46imq/6Cre7fZf23P2Aa/NQyFhCNymwbcJDMv6+6uU3THGn73lQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.42.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lKrr9bfYVLXXX0/p0tB3VB2zMbCgw+8CZkWd5U2d2idr7CORH0efKD+0aZukMFfg10qBaIouhFdFn5iR+34i5w=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.45.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nNdgmOZUkP+yR/yF0RsXapJNioORgnrA2Jl58ExlxyGUbHvHjcSAlNY7dsBljQFHhFYzBOh4NPs3TBbF681+qw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.42.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-536coihEiLB8E9wuSGG4j+f/9QhGQhvbb9WWF3Y+Ogn4Zz89Vm7vIQbre/M5coLLFIzVhLDoBD77QjtE+eXn0g=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-memcached": ["@opentelemetry/instrumentation-memcached@0.41.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/memcached": "^2.2.6" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Qrp+yl6pobVAm2F5AJizopDFtKkxwIzJ8iSnV1TDhbB8O7ct4N9p8rz3WvA3XAikS0bVw9rh/cRgYvb7g6AQcQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.49.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3qIvelQxqj+znuHB6f2sLGmTG6FUbpX0qsxABEG3yPh7i11f2dJ554bUxkpVV1Y9YafP3iKEHo2ybbjjUm5xyg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.44.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gBwxWvUFxTcXDXiLTqpiM7jyOS27X5x8saQesG8RsL128yxAoN3oiy3Hn3hIw13nkh+AHTXBTiADVD/lkazuiA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.43.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mysql": "2.15.26" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Yd4QLENitUAovh5JKbDIvzLVkt+3InnQYiWqcD4X7VjUGdVlZuCgMNkyUl6ML3WonH60jDy7S2rmLZAlWm7qTg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.43.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9W1AxMfrZV3ZeYBPjz8bkMRIRf1od4h+QZLw+m575lu41DMQIprcHXRZbyZRXZG+tgqM3YNBiNZCI2bDV3x46Q=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-nestjs-core": ["@opentelemetry/instrumentation-nestjs-core@0.42.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+JRi91A2Ue8JOY7WJ3oSq4HFB6+qIQQ62uu77fKLqV0xn0ft8YX/hDJceUJEKgqPlJMbHH5ppZlCrSPc/d3t0w=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-net": ["@opentelemetry/instrumentation-net@0.41.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3IqTpOaNxnCaCzCcFFPwGmX+b626Gx/uSHe61kP1kVDzhIKpwhgrzwWstdI2ZEzMa1jpNzharque/y9wEpsg8A=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.48.0", "", { "dependencies": { "@opentelemetry/core": "^1.26.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "1.27.0", "@opentelemetry/sql-common": "^0.40.1", "@types/pg": "8.6.1", "@types/pg-pool": "2.0.6" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-z0eG0A6SUXM/zSBisFVYrcp6aYbO8z1+R7cM7hxURBm8ccS98kVvZ+9UpLFd61YpSeof4bGhFsA8wqgNgqh4Vg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-pino": ["@opentelemetry/instrumentation-pino@0.44.0", "", { "dependencies": { "@opentelemetry/api-logs": "^0.55.0", "@opentelemetry/core": "^1.25.0", "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nyu6A1Zq3z/GUsfIJLsEMmUZrdqdVeQSESx8i7PzvUiVYyEdvf8w1sg4oPCBrSwl0PFU7FR4uYR4d04/QxFCoA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.44.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-QKBrjwHSejj/31JpxyI6wWEFK6ZqPmY/5ARFvzd7jSuTNtH2lMQ+Gb0j1T5hLJ6j3dDtFceYnC7CGXTSsx1jxg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-redis-4": ["@opentelemetry/instrumentation-redis-4@0.44.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mT4iGxqBeD4vUd2Dp5QG2UxaduWENHzsiPEgFvsPwSDARkyCXbTxCyOoXTTR53Vb4L8EklprbRBjukbljCdMTA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-restify": ["@opentelemetry/instrumentation-restify@0.43.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gNO8cAF7lPCCcWOPlx17LLTKKz2+jKkHI4OGhNoM+yUCG2KXBD5cZ8+XzL/EVLRL0GXHgV4Un4eeBnCUjXYTOw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-router": ["@opentelemetry/instrumentation-router@0.42.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bA0gmEIOZCkCbrnzWU5auSWPlEcU72URka0nQq3H+zoDaToO+Yi1756h9g5jL/9gx6YFzO5+ufRqVh4tNzf2Jw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-socket.io": ["@opentelemetry/instrumentation-socket.io@0.44.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Gf53pjHae88FrFY6eUHBGylJcFp90zd4HM5JlrIrTRfM28im7IijsCPSgMYez2m8Anr72aWrEoRtOJWfo7tE0Q=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.16.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.55.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mIzPC0fioXb9KQOm03UgGZDXwSBzYdCIT/6+S4jYHquLeVJvfKe4ivZo7bfNV0yHzfINpOefog76wlZ94tr3OA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.8.0", "", { "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-XUab3nrvk2CPjOTlIPJNUv3v0KIpK6flxF67Re6PoxVaxtN4Zh5hfUTowndn7rXMGwz2feO5LpDWjqfMQw8veQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-winston": ["@opentelemetry/instrumentation-winston@0.42.0", "", { "dependencies": { "@opentelemetry/api-logs": "^0.55.0", "@opentelemetry/instrumentation": "^0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kaMbm2oITQpX6q59gOsv5dPuZEXzLNnQYZiICg5P0XdsVCQkbvmWK3xoPhHTgdXUyhgIHc5uUiMknHmHfXqMQQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/resource-detector-alibaba-cloud": ["@opentelemetry/resource-detector-alibaba-cloud@0.29.7", "", { "dependencies": { "@opentelemetry/core": "^1.26.0", "@opentelemetry/resources": "^1.10.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-PExUl/R+reSQI6Y/eNtgAsk6RHk1ElYSzOa8/FHfdc/nLmx9sqMasBEpLMkETkzDP7t27ORuXe4F9vwkV2uwwg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/resource-detector-aws": ["@opentelemetry/resource-detector-aws@1.12.0", "", { "dependencies": { "@opentelemetry/core": "^1.0.0", "@opentelemetry/resources": "^1.10.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/resource-detector-azure": ["@opentelemetry/resource-detector-azure@0.3.0", "", { "dependencies": { "@opentelemetry/core": "^1.25.1", "@opentelemetry/resources": "^1.10.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-MFKiCQ+rUxCwJJH0ZLcdtsJ6FK/vLERsBhcu5pKHPSupdauVPaR5iRibApoF9dxZ1wuG5f+BRFO+USGdZXorDg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/resource-detector-container": ["@opentelemetry/resource-detector-container@0.5.3", "", { "dependencies": { "@opentelemetry/core": "^1.26.0", "@opentelemetry/resources": "^1.10.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-x5DxWu+ZALBuFpxwO2viv9ktH4Y3Gk9LaYKn2U8J+aeD412iy/OcGLPbQ76Px7pQ8qaJ5rnjcevBOHYT4aA+zQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/resource-detector-gcp": ["@opentelemetry/resource-detector-gcp@0.30.0", "", { "dependencies": { "@opentelemetry/core": "^1.0.0", "@opentelemetry/resources": "^1.10.0", "@opentelemetry/semantic-conventions": "^1.27.0", "gcp-metadata": "^6.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-lEbeiPEQtD+JGknF1ZZ6W7hsr1Ul9V27S68tIaPrY6WNdnuTL/7vcZSKHO8eu6NnCNJ7Up9oGFloMb2sfUazig=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.55.0", "@opentelemetry/exporter-logs-otlp-http": "0.55.0", "@opentelemetry/exporter-logs-otlp-proto": "0.55.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.55.0", "@opentelemetry/exporter-trace-otlp-http": "0.55.0", "@opentelemetry/exporter-trace-otlp-proto": "0.55.0", "@opentelemetry/exporter-zipkin": "1.28.0", "@opentelemetry/instrumentation": "0.55.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "@opentelemetry/sdk-trace-node": "1.28.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-gSXQWV23+9vhbjsvAIeM0LxY3W8DTKI3MZlzFp61noIb1jSr46ET+qoUjHlfZ1Yymebv9KXWeZsqhft81HBXuQ=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@1.26.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw=="], + + "traceroot-sdk-ts/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.53.0", "", { "dependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@1.26.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.53.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0", "@opentelemetry/sdk-logs": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-x5ygAQgWAQOI+UOhyV3z9eW7QU2dCfnfOuIBiyYmC2AWr74f6x/3JBnP27IAcEx6aihpqBYWKnpoUTztkVPAZw=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0", "@opentelemetry/sdk-logs": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-cSRKgD/n8rb+Yd+Cif6EnHEL/VZg1o8lEcEwFji1lwene6BdH51Zh3feAD9p2TyVoBKrl6Q9Zm2WltSp2k9gWQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-trace-base": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-jhEcVL1deeWNmTUP05UZMriZPSWUBcfg94ng7JuBb1q2NExgnADQFl1VQQ+xo62/JepK+MxQe4xAwlsDQFbISA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.53.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-grpc-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-m6KSh6OBDwfDjpzPVbuJbMgMbkoZfpxYH2r262KckgX9cMYvooWXEKzlJYsNDC6ADr28A1rtRoUVRwNfIN4tUg=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-T/bdXslwRKj23S96qbvGtaYOdfyew3TjPEKOk5mHjkCmkVl1O9C/YMdejwSsdLdOq2YW30KjR9kVi0YMxZushQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-PW5R34n3SJHO4t0UetyHKiXL6LixIqWN6lWncg3eRXhKuT30x+b7m5sDJS0kEWRfHeS+kG7uCw2vBzmB2lk3Dw=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-CPNYchBE7MBecCSVy0HKpUISEeJOniWqcHaAHpmasZ3j9o6V3AyBzhRc90jdmemq0HOxDr6ylhUbDhBqqPpeNw=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-olWQldtvbK4v22ymrKLbIcBi9L2SpMO84sCPY54IVsJhP9fRsxJT194C/AVaAuJzLE30EdhhM1VmvVYR7az+cw=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@1.26.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "1.26.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/propagator-b3": "1.26.0", "@opentelemetry/propagator-jaeger": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Fj5IVKrj0yeUwlewCRwzOVcr5avTuNnMHWf7GPc1t6WaT78J6CJyF3saZ/0RkZfdeNO8IcBl/bNcWMVZBMRW8Q=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.30.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg=="], + "tsc-alias/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "tsc-alias/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "winston-cloudwatch-logs/proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "winston-cloudwatch-logs/proxy-agent/http-proxy-agent": ["http-proxy-agent@4.0.1", "", { "dependencies": { "@tootallnate/once": "1", "agent-base": "6", "debug": "4" } }, "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg=="], + + "winston-cloudwatch-logs/proxy-agent/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "winston-cloudwatch-logs/proxy-agent/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent": ["pac-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "1", "agent-base": "6", "debug": "4", "get-uri": "3", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "5", "pac-resolver": "^5.0.0", "raw-body": "^2.2.0", "socks-proxy-agent": "5" } }, "sha512-CcFG3ZtnxO8McDigozwE3AqAw15zDvGH+OjXO4kzf7IkEKkQ4gxQ+3sdF50WmhQ4P/bVusXcqNE2S3XrNURwzQ=="], + + "winston-cloudwatch-logs/proxy-agent/socks-proxy-agent": ["socks-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "4", "socks": "^2.3.3" } }, "sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ=="], + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-sdk/node-http-handler/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-/jL/V1xdVRt5XppwiaEU8Etp5WHZj609n0xMTuehmCqdoOFbId1M+aEeDWZsQ+8JbEB/BJ6ynY2SlYmOaKtt8w=="], + "@hyperdx/node-opentelemetry/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-aws-lambda/@types/aws-lambda": ["@types/aws-lambda@8.10.147", "", {}, "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew=="], "@hyperdx/node-opentelemetry/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-aws-sdk/@opentelemetry/propagation-utils": ["@opentelemetry/propagation-utils@0.30.16", "", { "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw=="], @@ -3603,14 +4006,182 @@ "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + "colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "react-email/glob/path-scurry/lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-aws-lambda/@types/aws-lambda": ["@types/aws-lambda@8.10.143", "", {}, "sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-aws-sdk/@opentelemetry/propagation-utils": ["@opentelemetry/propagation-utils@0.30.16", "", { "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-bunyan/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-bunyan/@types/bunyan": ["@types/bunyan@1.8.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-connect/@types/connect": ["@types/connect@3.4.36", "", { "dependencies": { "@types/node": "*" } }, "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-grpc/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@1.28.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-http/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-ioredis/@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.36.2", "", {}, "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-mysql/@types/mysql": ["@types/mysql@2.15.26", "", { "dependencies": { "@types/node": "*" } }, "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-mysql2/@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.40.1", "", { "dependencies": { "@opentelemetry/core": "^1.1.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-pg/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-pg/@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.40.1", "", { "dependencies": { "@opentelemetry/core": "^1.1.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-pino/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-redis/@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.36.2", "", {}, "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-redis-4/@opentelemetry/redis-common": ["@opentelemetry/redis-common@0.36.2", "", {}, "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation-winston/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@1.28.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ZLwRMV+fNDpVmF2WYUdBHlq0eOWtEaUJSusrzjGnBt7iSRvfjFE3RXYUZJrqou/wIDWV0DwQ5KIfYe9WXg9Xqw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.55.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-grpc-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0", "@opentelemetry/sdk-logs": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ykqawCL0ILJWyCJlxCPSAlqQXZ6x2bQsxAVUu8S3z22XNqY5SMx0rl2d93XnvnrOwtcfm+sM9ZhbGh/i5AZ9xw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0", "@opentelemetry/sdk-logs": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fpFObWWq+DoLVrBU2dyMEaVkibByEkmKQZIUIjW/4j7lwIsTgW7aJCoD9RYFVB/tButcqov5Es2C0J2wTjM2tg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-trace-base": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-vjE+DxUr+cUpxikdKCPiLZM5Wx7g1bywjCG76TQocvsA7Tmbb9p0t1+8gPlu9AGH7VEzPwDxxpN4p1ajpOurzQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.55.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-grpc-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ohIkCLn2Wc3vhhFuf1bH8kOXHMEdcWiD847x7f3Qfygc+CGiatGLzQYscTcEYsWGMV22gVwB/kVcNcx5a3o8gA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lMiNic63EVHpW+eChmLD2CieDmwQBFi72+LFbh8+5hY0ShrDGrsGP/zuT5MRh7M/vM/UZYO/2A/FYd7CMQGR7A=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-qxiJFP+bBZW3+goHCGkE1ZdW9gJU0fR7eQ6OP+Rz5oGtEBbq4nkGodhb7C9FJlEFlE2siPtCxoeupV0gtYynag=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@1.28.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-AMwr3eGXaPEH7gk8yhcUcen31VXy1yU5VJETu0pCfGpggGCYmhm0FKgYBpL5/vlIgQJWU/sW2vIjCL7aSilpKg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@1.28.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-cIyXSVJjGeTICENN40YSvLDAq4Y2502hGK3iN7tfdynQLKWb3XWZQEkPc+eSx47kiy11YeFAlYkEfXwR1w8kfw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-TSx+Yg/d48uWW6HtjS1AD5x6WPfLhDWLl/WxC7I2fMevaiBuKCuraxTB8MDXieCNnBI24bw9ytyXrDCswFfWgA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.28.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-43tqMK/0BcKTyOvm15/WQ3HLr0Vu/ucAl/D84NO7iSlv6O4eOprxSHa3sUtmYkaZWHqdDJV0AHVz/R6u4JALVQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.28.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/semantic-conventions": "1.27.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ceUVWuCpIao7Y5xE02Xs3nQi0tOGmMea17ecBdwtCvdo9ekmO+ijc9RFDgfifMl7XCBf41zne/1POM3LqSTZDA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@1.28.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "1.28.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/propagator-b3": "1.28.0", "@opentelemetry/propagator-jaeger": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-N0sYfYXvHpP0FNIyc+UfhLnLSTOuZLytV0qQVrDWIlABeD/DWJIGttS7nYeR14gQLXch0M1DW8zm3VeN6Opwtg=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.53.0", "", { "dependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-dhSisnEgIj/vJZXZV6f6KcTnyLDx/VuQ6l3ejuZpMpPlh9S1qMHiZU9NMmOkVkwwHkMy3G6mEBwdP23vUZVr4g=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-0SvDXmou/JjzSDOjUmetAAvcKQW6ZrvosU0rkbDGpXvvZN+pQF6JbK/Kd4hNdK4q/22yeruqvukXEJyySTzyTQ=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.27.0", "", {}, "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.53.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.53.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-exporter-base": "0.53.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-F7RCN8VN+lzSa4fGjewit8Z5fEUpY/lmMVy5EWn2ZpbAabg3EE3sCLuTNfOiooNGnmvzimUPruoeqeko/5/TzQ=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.53.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.53.0", "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0", "@opentelemetry/sdk-logs": "0.53.0", "@opentelemetry/sdk-metrics": "1.26.0", "@opentelemetry/sdk-trace-base": "1.26.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rM0sDA9HD8dluwuBxLetUmoqGJKSAbWenwD65KY9iZhUxdBHRLrIdrABfNDP7aiTjcgK8XFyTn5fhDz7N+W6DA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.26.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-HedpXXYzzbaoutw6DFLWLDket2FwLkLpil4hGCZ1xYEIMTcivdfwEOISgdbLEWyG3HW52gTq2V9mOVJrONgiwg=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-vvVkQLQ/lGGyEy9GT8uFnI047pajSOVnZI2poJqVGD3nJ+B9sFGdlHNnQKophE3lHfnIH0pw2ubrCTjZCgIj+Q=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.26.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-DelFGkCdaxA1C/QA0Xilszfr0t4YbGd3DjxiCDPh34lfnFr+VkkrjV9S8ZTJvAzfdKERXhfOxIKBoGPJwoSz7Q=="], + "tsc-alias/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "winston-cloudwatch-logs/proxy-agent/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/get-uri": ["get-uri@3.0.2", "", { "dependencies": { "@tootallnate/once": "1", "data-uri-to-buffer": "3", "debug": "4", "file-uri-to-path": "2", "fs-extra": "^8.1.0", "ftp": "^0.3.10" } }, "sha512-+5s0SJbGoyiJTZZ2JTpFPLMPSch72KEqGOTvQsBqg0RBWvwhWUSYZFAtz3TPW0GXJuLBJPts1E241iHg+VRfhg=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver": ["pac-resolver@5.0.1", "", { "dependencies": { "degenerator": "^3.0.2", "ip": "^1.1.5", "netmask": "^2.0.2" } }, "sha512-cy7u00ko2KVgBAjuhevqpPeHIkCIqPe1v24cydhWjmeuzaBfmUWFCZJ1iAh5TuVzVZoUzXIW7K8sMYOZ84uZ9Q=="], + "@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.55.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gebbjl9FiSp52igWXuGjcWQKfB6IBwFGt5z1VFwTcVZVeEZevB6bJIqoFrhH4A02m7OUlpJ7l4EfRi3UtkNANQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.55.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-exporter-base": "0.55.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gebbjl9FiSp52igWXuGjcWQKfB6IBwFGt5z1VFwTcVZVeEZevB6bJIqoFrhH4A02m7OUlpJ7l4EfRi3UtkNANQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.55.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.55.0", "@opentelemetry/core": "1.28.0", "@opentelemetry/resources": "1.28.0", "@opentelemetry/sdk-logs": "0.55.0", "@opentelemetry/sdk-metrics": "1.28.0", "@opentelemetry/sdk-trace-base": "1.28.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kVqEfxtp6mSN2Dhpy0REo1ghP4PYhC1kMHQJ2qVlO99Pc+aigELjZDfg7/YKmL71gR6wVGIeJfiql/eXL7sQPA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.28.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-igcl4Ve+F1N2063PJUkesk/GkYyuGIWinYkSyAFTnIj3gzrOgvOA4k747XNdL47HRRL1w/qh7UW8NDuxOLvKFA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@1.28.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Q7HVDIMwhN5RxL4bECMT4BdbyYSAKkC6U/RGn4NpO/cbqP6ZRg+BS7fPo/pGZi2w8AHfpIGQFXQmE8d2PC5xxQ=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.28.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-wKJ94+s8467CnIRgoSRh0yXm/te0QMOwTq9J01PfG/RzYZvlvN8aRisN2oZ9SznB45dDGnMj3BhUlchSA9cEKA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA=="], + + "traceroot-sdk-ts/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.53.0", "", { "dependencies": { "@opentelemetry/core": "1.26.0", "@opentelemetry/otlp-transformer": "0.53.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-UCWPreGQEhD6FjBaeDuXhiMf6kkBODF0ZQzrk/tuQcaVDJ+dDQ/xhJp192H9yWnKxVpEjFrSSLnpqmX4VwX+eA=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/get-uri/data-uri-to-buffer": ["data-uri-to-buffer@3.0.1", "", {}, "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator": ["degenerator@3.0.4", "", { "dependencies": { "ast-types": "^0.13.2", "escodegen": "^1.8.1", "esprima": "^4.0.0", "vm2": "^3.9.17" } }, "sha512-Z66uPeBfHZAHVmue3HPfyKu2Q0rC2cRxbTOsvmU/po5fvvcx27W4mIu9n0PUlQih4oUYvcG1BsbtVv8x7KDOSw=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA=="], + + "traceroot-sdk-ts/@opentelemetry/auto-instrumentations-node/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.55.0", "", { "dependencies": { "@opentelemetry/core": "1.28.0", "@opentelemetry/otlp-transformer": "0.55.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iHQI0Zzq3h1T6xUJTVFwmFl5Dt5y1es+fl4kM+k5T/3YvmVyeYkSiF+wHCg6oKrlUAJfk+t55kaAu3sYmt7ZYA=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator/escodegen": ["escodegen@1.14.3", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator/escodegen/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator/escodegen/optionator": ["optionator@0.8.3", "", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator/escodegen/optionator/levn": ["levn@0.3.0", "", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator/escodegen/optionator/prelude-ls": ["prelude-ls@1.1.2", "", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], + + "winston-cloudwatch-logs/proxy-agent/pac-proxy-agent/pac-resolver/degenerator/escodegen/optionator/type-check": ["type-check@0.3.2", "", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="], } } diff --git a/vite/src/views/products/product/product-item/CreateProductItem2.tsx b/vite/src/views/products/product/product-item/CreateProductItem2.tsx index 8e102579d..b9782dec4 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem2.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem2.tsx @@ -103,7 +103,7 @@ export function CreateProductItem2({ className={cn("w-full", classNames?.button)} startIcon={} > - Add Product Item + Add item to product
diff --git a/vite/src/views/products/product/product-item/create-product-item/CreateFeatureFromItem.tsx b/vite/src/views/products/product/product-item/create-product-item/CreateFeatureFromItem.tsx deleted file mode 100644 index 65af55932..000000000 --- a/vite/src/views/products/product/product-item/create-product-item/CreateFeatureFromItem.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { CreateFeature } from "@/views/features/CreateFeature"; -import { useProductItemContext } from "../ProductItemContext"; -import { - ProductItemInterval, - ProductItem, - CreateFeature as CreateFeatureType, -} from "@autumn/shared"; -import { useProductContext } from "../../ProductContext"; -import { DialogContentWrapper } from "@/components/general/modal-components/DialogContentWrapper"; -import { useFeatureDialogState } from "@/views/features/hooks/useFeatureDialogState"; - -export const CreateFeatureFromItem = () => { - const { setShowCreateFeature, setOpen, open, setItem, item } = - useProductItemContext(); - - const { features, setFeatures } = useProductContext(); - - const setSelectedFeature = (feature: CreateFeatureType) => { - setFeatures([...features, feature]); - setItem({ ...item, feature_id: feature.id! }); - }; - - const { - feature, - setFeature, - eventNameInput, - setEventNameInput, - eventNameChanged, - setEventNameChanged, - } = useFeatureDialogState({ - entityCreate: true, - }); - - return ( - <> - - Create Feature - - - - ); - - return ( - <> - -
- {features.length > 0 && ( - - )} - - - {/* {showCreateFeature || (features.length == 0 && item.price === null) - ? "Create Feature" - : "Add Product Item"} */} - Create Feature - -
-
-
-
- -
- {/* {showCreateFeature || (features.length == 0 && item.price === null) ? ( -
- -
- ) : ( -
- -
- )} */} -
- - ); -}; From 5c91f4dbe8c04a9c568f9bedc3f7d71e13142150 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 5 Aug 2025 14:13:13 -0700 Subject: [PATCH 15/37] fix: create stripe price --- server/src/check.ts | 9 ++-- server/src/errors/logger.ts | 2 +- server/src/external/logtail/logtailUtils.ts | 3 +- .../createStripePrice/createStripePrice.ts | 13 ++++-- .../internal/customers/attach/handleAttach.ts | 2 +- .../utils/scriptUtils/logUtils/logSubItems.ts | 11 ++++- .../productModels/entModels/entTable.ts | 1 + vite/src/utils/product/itemIntervalUtils.ts | 2 +- .../components/ConfigWithFeature.tsx | 2 - .../components/SelectResetCycle.tsx | 46 ++++++++++++++----- 10 files changed, 63 insertions(+), 28 deletions(-) diff --git a/server/src/check.ts b/server/src/check.ts index 7185b07e7..1381f4c89 100644 --- a/server/src/check.ts +++ b/server/src/check.ts @@ -33,13 +33,14 @@ import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import { getRelatedCusPrice } from "./internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { CusProductService } from "./internal/customers/cusProducts/CusProductService.js"; +import { logSubItems } from "./utils/scriptUtils/logUtils/logSubItems.js"; const { db, client } = initDrizzle({ maxConnections: 5 }); let orgSlugs = process.env.ORG_SLUGS!.split(","); const skipEmails = process.env.SKIP_EMAILS!.split(","); +orgSlugs = ["athenahq"]; -orgSlugs = []; const getSingleCustomer = async ({ stripeCli, customerId, @@ -136,7 +137,7 @@ const checkCustomerCorrect = async ({ "number of stripe subs should be the same as number of subscription ids" ); - const subItems = stripeSubs.flatMap((sub: any) => sub.items.data); + let subItems = stripeSubs.flatMap((sub: any) => sub.items.data); const prices = cusProductToPrices({ cusProduct }); @@ -174,7 +175,7 @@ const checkCustomerCorrect = async ({ let expectedQuantity = options?.upcoming_quantity || options?.quantity; - console.log("Sub item: ", subItem); + // console.log("Sub item: ", subItem); assert( subItem?.quantity == expectedQuantity, `sub item quantity for prepaid price (featureId: ${featureId}) should be ${expectedQuantity}` @@ -282,7 +283,7 @@ export const check = async () => { let customerId; - // customerId = "SwZ6cDgOFAY9S29nHigvPaUk4uq2"; + // customerId = "94fd7303-c8ae-4873-a6cd-0f5241aef232"; let customers: FullCustomer[] = []; let stripeSubs: Stripe.Subscription[] = []; diff --git a/server/src/errors/logger.ts b/server/src/errors/logger.ts index dc5c81753..13de1dffd 100644 --- a/server/src/errors/logger.ts +++ b/server/src/errors/logger.ts @@ -55,7 +55,7 @@ export const initLogger = () => { }, }, // Use multistream to send logs to multiple destinations - pino.multistream(streams), + pino.multistream(streams) ); return logger; diff --git a/server/src/external/logtail/logtailUtils.ts b/server/src/external/logtail/logtailUtils.ts index 89f054f35..a51df8e75 100644 --- a/server/src/external/logtail/logtailUtils.ts +++ b/server/src/external/logtail/logtailUtils.ts @@ -32,6 +32,7 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => { const strings = args .filter((arg) => typeof arg === "string") .map(rewriteAppPath); + const objects = args .filter((arg) => typeof arg !== "string" && arg !== null) .map((obj) => (obj instanceof Error ? rewriteErrorStack(obj) : obj)); @@ -44,7 +45,7 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => { const errorObject = args.find((arg) => arg instanceof Error); if (errorObject) { message = rewriteAppPath( - errorObject.stack || errorObject.message || "Error occurred", + errorObject.stack || errorObject.message || "Error occurred" ); } } diff --git a/server/src/external/stripe/createStripePrice/createStripePrice.ts b/server/src/external/stripe/createStripePrice/createStripePrice.ts index 8b965a652..ad2d847a4 100644 --- a/server/src/external/stripe/createStripePrice/createStripePrice.ts +++ b/server/src/external/stripe/createStripePrice/createStripePrice.ts @@ -25,6 +25,7 @@ import { } from "./createStripeArrearProrated.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; +import RecaseError from "@/utils/errorUtils.js"; export const checkCurStripePrice = async ({ price, @@ -46,10 +47,7 @@ export const checkCurStripePrice = async ({ expand: ["product"], }); - if ( - !stripePrice.active || - !(stripePrice.product as Stripe.Product).active - ) { + if (!stripePrice.active) { stripePrice = null; } } catch (error) { @@ -122,7 +120,12 @@ export const createStripePriceIFNotExist = async ({ billingType == BillingType.OneOff ) { if (!stripePrice) { - logger.info("Creating stripe fixed price"); + // logger.info("Creating stripe fixed price: ", { + // data: { + // price, + // stripePrice, + // }, + // }); await createStripeFixedPrice({ db, stripeCli, diff --git a/server/src/internal/customers/attach/handleAttach.ts b/server/src/internal/customers/attach/handleAttach.ts index 64f6dcf6d..a137a6e44 100644 --- a/server/src/internal/customers/attach/handleAttach.ts +++ b/server/src/internal/customers/attach/handleAttach.ts @@ -6,7 +6,7 @@ import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js"; import { getAttachBranch } from "./attachUtils/getAttachBranch.js"; import { getAttachConfig } from "./attachUtils/getAttachConfig.js"; import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js"; -import { checkStripeConnections, createStripePrices } from "./attachRouter.js"; +import { checkStripeConnections } from "./attachRouter.js"; import { insertCustomItems } from "./attachUtils/insertCustomItems.js"; import { runAttachFunction } from "./attachUtils/getAttachFunction.js"; diff --git a/server/src/utils/scriptUtils/logUtils/logSubItems.ts b/server/src/utils/scriptUtils/logUtils/logSubItems.ts index 6b410a32a..679087de7 100644 --- a/server/src/utils/scriptUtils/logUtils/logSubItems.ts +++ b/server/src/utils/scriptUtils/logUtils/logSubItems.ts @@ -1,8 +1,15 @@ import { subItemToAutumnInterval } from "@/external/stripe/utils.js"; import Stripe from "stripe"; -export const logSubItems = (sub: Stripe.Subscription) => { - for (const item of sub.items.data) { +export const logSubItems = ({ + sub, + subItems, +}: { + sub?: Stripe.Subscription; + subItems?: Stripe.SubscriptionItem[]; +}) => { + let finalSubItems = subItems || sub!.items.data; + for (const item of finalSubItems) { let isMetered = item.price.recurring?.usage_type === "metered"; let isTiered = item.price.billing_scheme === "tiered"; diff --git a/shared/models/productModels/entModels/entTable.ts b/shared/models/productModels/entModels/entTable.ts index 26718ba01..2f0c8f878 100644 --- a/shared/models/productModels/entModels/entTable.ts +++ b/shared/models/productModels/entModels/entTable.ts @@ -28,6 +28,7 @@ export const entitlements = pgTable( allowance_type: text(), allowance: numeric({ mode: "number" }), interval: text(), + // interval_count: numeric({ mode: "number" }).default(1), carry_from_previous: boolean("carry_from_previous").default(false), entity_feature_id: text("entity_feature_id").default(sql`null`), diff --git a/vite/src/utils/product/itemIntervalUtils.ts b/vite/src/utils/product/itemIntervalUtils.ts index 961e07482..42f5f4243 100644 --- a/vite/src/utils/product/itemIntervalUtils.ts +++ b/vite/src/utils/product/itemIntervalUtils.ts @@ -39,5 +39,5 @@ export const itemToEntInterval = (item: ProductItem) => { return EntInterval.Lifetime; } - return item.interval; + return item.interval as unknown as EntInterval; }; diff --git a/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx b/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx index f411fd168..0a4ceac3b 100644 --- a/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx +++ b/vite/src/views/products/product/product-item/components/ConfigWithFeature.tsx @@ -5,8 +5,6 @@ import { useProductContext } from "../../ProductContext"; import { FeatureType } from "@autumn/shared"; import { getFeature } from "@/utils/product/entitlementUtils"; import { FeatureConfig } from "../product-item-config/FeatureItemConfig"; -import { useEffect } from "react"; -import { CreateItemStep } from "../utils/CreateItemStep"; export const ConfigWithFeature = ({ show, diff --git a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx index 7de1f0f7d..c2e96b680 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx @@ -15,6 +15,8 @@ import { cn } from "@/lib/utils"; import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; import { getFeatureUsageType } from "@/utils/product/entitlementUtils"; import { useProductContext } from "../../../ProductContext"; +import { Button } from "@/components/ui/button"; +import { ArrowUp01, PlusIcon } from "lucide-react"; export const SelectResetCycle = () => { const { features } = useProductContext(); @@ -34,11 +36,20 @@ export const SelectResetCycle = () => { return null; } + const interval = itemToEntInterval(item); + + const getIntervalText = (interval: EntInterval) => { + return interval === "semi_annual" + ? "per half year" + : interval === "lifetime" + ? "no reset" + : `per ${interval}`; + }; return (
@@ -58,18 +69,31 @@ export const SelectResetCycle = () => { }} > - + + {getIntervalText(interval)} + - {Object.values(EntInterval).map((interval) => ( - - {interval === "semi_annual" - ? "per half year" - : interval === "lifetime" - ? "no reset" - : `per ${interval}`} - - ))} + {Object.values(EntInterval).map((interval) => { + return ( + +
+ {getIntervalText(interval)} + {/* */} +
+
+ ); + })}
From 2ee3ebd790272799d091aed95df1e90254f558a7 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 6 Aug 2025 09:00:38 -0700 Subject: [PATCH 16/37] adding popover to select interval --- server/src/errors/logger.ts | 128 +++++++++++++++--- .../internal/customers/attach/handleAttach.ts | 22 +++ .../productModels/entModels/entTable.ts | 2 +- vite/src/components/ui/select.tsx | 30 ++-- .../components/SelectResetCycle.tsx | 78 +++++++---- 5 files changed, 203 insertions(+), 57 deletions(-) diff --git a/server/src/errors/logger.ts b/server/src/errors/logger.ts index 13de1dffd..21a38b1ca 100644 --- a/server/src/errors/logger.ts +++ b/server/src/errors/logger.ts @@ -1,4 +1,111 @@ import pino from "pino"; +import { Writable } from "stream"; + +// Custom log formatter for Bun compatibility +const createDevLogStream = () => { + const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", + gray: "\x1b[90m", + bgRed: "\x1b[41m", + }; + + const levelColors: Record = { + // Numeric levels + 10: colors.gray, // trace + 20: colors.blue, // debug + 30: colors.green, // info + 40: colors.yellow, // warn + 50: colors.red, // error + 60: colors.bgRed, // fatal + // String levels + TRACE: colors.gray, + DEBUG: colors.blue, + INFO: colors.green, + WARN: colors.yellow, + ERROR: colors.red, + FATAL: colors.bgRed, + }; + + const levelNames: Record = { + // Numeric levels + 10: "TRACE", + 20: "DEBUG", + 30: "INFO", + 40: "WARN", + 50: "ERROR", + 60: "FATAL", + // String levels (pass through) + TRACE: "TRACE", + DEBUG: "DEBUG", + INFO: "INFO", + WARN: "WARN", + ERROR: "ERROR", + FATAL: "FATAL", + }; + + return new Writable({ + write(chunk, encoding, callback) { + try { + const log = JSON.parse(chunk.toString()); + const timestamp = new Date(log.time) + .toISOString() + .replace("T", " ") + .replace("Z", ""); + const level = log.level; + const levelColor = levelColors[level] || colors.white; + const levelName = + levelNames[level] || (typeof level === "string" ? level : "UNKNOWN"); + + // Format the message + let message = log.msg || ""; + + // Add any additional fields (excluding standard pino fields) + const excludeFields = [ + "time", + "level", + "msg", + "pid", + "hostname", + "res", + "statusCode", + "worker", + "context", + "req", + // "data", + ]; + const additionalFields = Object.keys(log) + .filter((key) => !excludeFields.includes(key)) + .reduce((acc, key) => { + acc[key] = log[key]; + return acc; + }, {} as any); + + if (Object.keys(additionalFields).length > 0) { + message += " " + JSON.stringify(additionalFields, null, 2); + } + + // Format the final log line + const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}\n`; + + process.stdout.write(formattedLog); + callback(); + } catch (error) { + // Fallback for malformed JSON + process.stdout.write(chunk); + callback(); + } + }, + }); +}; export const initLogger = () => { // Create separate streams for console and HyperDX @@ -7,26 +114,7 @@ export const initLogger = () => { if (process.env.NODE_ENV === "development") { streams.push({ level: process.env.NODE_ENV === "development" ? "debug" : "info", - stream: pino.transport({ - target: "pino-pretty", - options: { - colorize: true, - translateTime: "UTC:yyyy-mm-dd HH:MM:ss", - ignore: "pid,hostname,res,statusCode,worker,context,req", - customColors: { - default: "white", - 60: "bgRed", - 50: "red", - 40: "yellow", - 30: "green", - 20: "blue", - 10: "gray", - message: "reset", - greyMessage: "gray", - time: "darkGray", - }, - }, - }), + stream: createDevLogStream(), }); } diff --git a/server/src/internal/customers/attach/handleAttach.ts b/server/src/internal/customers/attach/handleAttach.ts index a137a6e44..99e14fd98 100644 --- a/server/src/internal/customers/attach/handleAttach.ts +++ b/server/src/internal/customers/attach/handleAttach.ts @@ -59,6 +59,28 @@ export const handleAttach = async (req: any, res: any) => customEnts: customEnts || [], }); + try { + req.logger.info(`Attach params: `, { + data: { + products: attachParams.products.map((p) => ({ + id: p.id, + name: p.name, + processor: p.processor, + version: p.version, + })), + prices: attachParams.prices.map((p) => ({ + id: p.id, + config: p.config, + })), + entitlements: attachParams.entitlements.map((e) => ({ + internal_feature_id: e.internal_feature_id, + feature_id: e.feature_id, + })), + freeTrial: attachParams.freeTrial, + }, + }); + } catch (error) {} + await runAttachFunction({ req, res, diff --git a/shared/models/productModels/entModels/entTable.ts b/shared/models/productModels/entModels/entTable.ts index 2f0c8f878..1f1919e7b 100644 --- a/shared/models/productModels/entModels/entTable.ts +++ b/shared/models/productModels/entModels/entTable.ts @@ -28,7 +28,7 @@ export const entitlements = pgTable( allowance_type: text(), allowance: numeric({ mode: "number" }), interval: text(), - // interval_count: numeric({ mode: "number" }).default(1), + interval_count: numeric({ mode: "number" }).default(1), carry_from_previous: boolean("carry_from_previous").default(false), entity_feature_id: text("entity_feature_id").default(sql`null`), diff --git a/vite/src/components/ui/select.tsx b/vite/src/components/ui/select.tsx index c78c249ae..415593917 100644 --- a/vite/src/components/ui/select.tsx +++ b/vite/src/components/ui/select.tsx @@ -48,7 +48,7 @@ function SelectTrigger({ transition-colors duration-100 p-2 `, - className, + className )} {...props} > @@ -94,7 +94,7 @@ data-[placeholder]:text-t3 transition-colors duration-100 p-2 `, - className, + className )} {...props} > @@ -120,7 +120,7 @@ function SelectContent({ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", - className, + className )} position={position} {...props} @@ -130,7 +130,7 @@ function SelectContent({ className={cn( "p-1", position === "popper" && - "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1", + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1" )} > {children} @@ -157,22 +157,30 @@ function SelectLabel({ function SelectItem({ className, children, + endComponent, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + endComponent?: React.ReactNode; +}) { return ( - - - + {endComponent ? ( + endComponent + ) : ( + + + + )} + {children} ); @@ -200,7 +208,7 @@ function SelectScrollUpButton({ data-slot="select-scroll-up-button" className={cn( "flex cursor-default items-center justify-center py-1", - className, + className )} {...props} > @@ -218,7 +226,7 @@ function SelectScrollDownButton({ data-slot="select-scroll-down-button" className={cn( "flex cursor-default items-center justify-center py-1", - className, + className )} {...props} > diff --git a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx index c2e96b680..ac45da7e9 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx @@ -16,7 +16,16 @@ import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; import { getFeatureUsageType } from "@/utils/product/entitlementUtils"; import { useProductContext } from "../../../ProductContext"; import { Button } from "@/components/ui/button"; -import { ArrowUp01, PlusIcon } from "lucide-react"; +import { ArrowUp01, CheckIcon, PlusIcon } from "lucide-react"; +import { useState } from "react"; + +const getIntervalText = (interval: EntInterval) => { + return interval === "semi_annual" + ? "per half year" + : interval === "lifetime" + ? "no reset" + : `per ${interval}`; +}; export const SelectResetCycle = () => { const { features } = useProductContext(); @@ -38,13 +47,6 @@ export const SelectResetCycle = () => { const interval = itemToEntInterval(item); - const getIntervalText = (interval: EntInterval) => { - return interval === "semi_annual" - ? "per half year" - : interval === "lifetime" - ? "no reset" - : `per ${interval}`; - }; return (
{ - {Object.values(EntInterval).map((interval) => { + {Object.values(EntInterval).map((intervalOption) => { + const isSelected = intervalOption === interval; return ( - -
- {getIntervalText(interval)} - {/* */} -
-
+ ); })}
@@ -99,3 +91,39 @@ export const SelectResetCycle = () => {
); }; + +const SelectIntervalItem = ({ + interval, + isSelected, +}: { + interval: EntInterval; + isSelected: boolean; +}) => { + const [hover, setHover] = useState(false); + + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + endComponent={ + // isSelected && + hover && ( + + ) + } + > +
+ {getIntervalText(interval)} +
+
+ ); +}; From b06db7758d814268f3980a804ad798b70f1c9f3d Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 6 Aug 2025 09:17:29 -0700 Subject: [PATCH 17/37] fix: renewal with force_checkout --- .../attach/attachUtils/handleAttachErrors.ts | 1 - server/tests/attach/renew/renew1.ts | 131 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 server/tests/attach/renew/renew1.ts diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts index 7c61ae6af..396dabac5 100644 --- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts +++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts @@ -213,7 +213,6 @@ export const handleAttachErrors = async ({ AttachBranch.NewVersion, AttachBranch.SameCustom, AttachBranch.UpdatePrepaidQuantity, - AttachBranch.Renew, ]; if (updateProductFlows.includes(branch)) { handleNonCheckoutErrors({ diff --git a/server/tests/attach/renew/renew1.ts b/server/tests/attach/renew/renew1.ts new file mode 100644 index 000000000..540e16230 --- /dev/null +++ b/server/tests/attach/renew/renew1.ts @@ -0,0 +1,131 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { + APIVersion, + AppEnv, + AttachBranch, + CreateFreeTrialSchema, + CusProductStatus, + FreeTrialDuration, + Organization, + organizations, +} from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { addPrefixToProducts, runAttachTest } from "../utils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { addDays } from "date-fns"; +import { expect } from "chai"; +import { eq } from "drizzle-orm"; +import { CacheManager } from "@/external/caching/CacheManager.js"; +import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; + +const testCase = "renew1"; + +export let free = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + }), + ], + isDefault: false, + type: "free", +}); + +export let pro = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Credits, + includedUsage: 1000, + }), + ], + + type: "pro", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing renew pro, force checkout`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [free, pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [free, pro], + db, + orgId: org.id, + env, + }); + }); + + it("should attach pro product", async function () { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + }); + + it("should attach free, then pro with force checkout and renew", async function () { + await autumn.attach({ + customer_id: customerId, + product_id: free.id, + }); + + // 1. Get attach preview + let attachPreview = await autumn.attachPreview({ + customer_id: customerId, + product_id: pro.id, + }); + + expect(attachPreview.branch).to.equal(AttachBranch.Renew); + + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + force_checkout: true, + }); + + let customer = await autumn.customers.get(customerId); + + expectProductAttached({ + customer, + product: pro, + }); + }); +}); From ab41aad5031793b827826c0974627137dfca9df3 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 6 Aug 2025 09:55:50 -0700 Subject: [PATCH 18/37] fix: product filters --- server/src/external/resend/resendUtils.ts | 2 +- server/src/internal/products/internalProductRouter.ts | 3 +++ vite/src/views/customers/CustomersView.tsx | 4 +--- .../product-item-config/components/SelectResetCycle.tsx | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/external/resend/resendUtils.ts b/server/src/external/resend/resendUtils.ts index 76c6e8d5d..9862ff260 100644 --- a/server/src/external/resend/resendUtils.ts +++ b/server/src/external/resend/resendUtils.ts @@ -14,7 +14,7 @@ export interface ResendEmailProps { } export const nameToEmail = (name: string) => { - return `${name.toLowerCase().replace(/\s+/g, ".")}@${process.env.RESEND_DOMAIN}`; + return `${name.toLowerCase().replace(/\s+/g, ".")}@hey.${process.env.RESEND_DOMAIN}`; }; export const sendTextEmail = async ({ diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index 7e78e1e8c..b817f6aa4 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -27,6 +27,8 @@ productRouter.get("/data", async (req: any, res) => { try { let { db } = req; + const allVersions = req.query.all_versions === "true"; + const [products, features, org, coupons, rewardPrograms] = await Promise.all([ ProductService.listFull({ @@ -34,6 +36,7 @@ productRouter.get("/data", async (req: any, res) => { orgId: req.orgId, env: req.env, archived: false, + returnAll: allVersions, }), FeatureService.getFromReq(req), OrgService.getFromReq(req), diff --git a/vite/src/views/customers/CustomersView.tsx b/vite/src/views/customers/CustomersView.tsx index 83d52ecc0..1635154ec 100644 --- a/vite/src/views/customers/CustomersView.tsx +++ b/vite/src/views/customers/CustomersView.tsx @@ -48,13 +48,11 @@ function CustomersView({ env }: { env: AppEnv }) { const [paginationLoading, setPaginationLoading] = React.useState(false); const { data: productsData, isLoading: productsLoading } = useAxiosSWR({ - url: `/products/data`, - env, + url: `/products/data?all_versions=true`, }); const { data: savedViewsData, mutate: mutateSavedViews } = useAxiosSWR({ url: "/saved_views", - env, }); const { data, isLoading, error, mutate } = useAxiosPostSWR({ diff --git a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx index c2e96b680..c09d97081 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx @@ -79,9 +79,9 @@ export const SelectResetCycle = () => { -
+
{getIntervalText(interval)} {/* + + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > +
+ Interval Count +
+
+ setIntervalCount(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSave(); + } + if (e.key === "Escape") { + setOpen(false); + } + }} + /> + +
+
+ + ); +}; diff --git a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx index ac45da7e9..884a6f92c 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/SelectResetCycle.tsx @@ -8,7 +8,12 @@ import { import { Select } from "@/components/ui/select"; import { useProductItemContext } from "../../ProductItemContext"; import { isFeaturePriceItem } from "@/utils/product/getItemType"; -import { EntInterval, FeatureUsageType, Infinite } from "@autumn/shared"; +import { + BillingInterval, + EntInterval, + FeatureUsageType, + Infinite, +} from "@autumn/shared"; import { itemToEntInterval } from "@/utils/product/itemIntervalUtils"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; import { cn } from "@/lib/utils"; @@ -18,13 +23,37 @@ import { useProductContext } from "../../../ProductContext"; import { Button } from "@/components/ui/button"; import { ArrowUp01, CheckIcon, PlusIcon } from "lucide-react"; import { useState } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { ToggleDisplayButton } from "@/components/general/ToggleDisplayButton"; +import { Input } from "@/components/ui/input"; +import { CustomiseIntervalPopover } from "./CusomiseIntervalPopover"; -const getIntervalText = (interval: EntInterval) => { - return interval === "semi_annual" +const getIntervalText = ({ + interval, + intervalCount, + billingInterval, +}: { + interval?: EntInterval; + billingInterval?: BillingInterval; + intervalCount?: number; +}) => { + const finalInterval = interval ?? billingInterval; + if ( + finalInterval === EntInterval.Lifetime || + finalInterval === BillingInterval.OneOff + ) { + return "no reset"; + } + if (intervalCount && intervalCount > 1) { + return `per ${intervalCount} ${finalInterval}s`; + } + return finalInterval === BillingInterval.SemiAnnual ? "per half year" - : interval === "lifetime" - ? "no reset" - : `per ${interval}`; + : `per ${finalInterval}`; }; export const SelectResetCycle = () => { @@ -63,35 +92,101 @@ export const SelectResetCycle = () => { - +
+ +
); }; +// const SelectIntervalCountPopover = () => { +// const [open, setOpen] = useState(false); +// const { item, setItem } = useProductItemContext(); +// const [intervalCount, setIntervalCount] = useState(item.interval_count || 1); + +// const handleSave = () => { +// setItem({ +// ...item, +// interval_count: parseInt(intervalCount || 1), +// }); +// setOpen(false); +// }; + +// return ( +// +// +// +// +// e.preventDefault()} +// onCloseAutoFocus={(e) => e.preventDefault()} +// > +//
+// Interval Count +//
+//
+// setIntervalCount(e.target.value)} +// onKeyDown={(e) => { +// if (e.key === "Enter") { +// handleSave(); +// } +// if (e.key === "Escape") { +// setOpen(false); +// } +// }} +// /> +// +//
+//
+//
+// ); +// }; + const SelectIntervalItem = ({ interval, isSelected, @@ -99,6 +194,7 @@ const SelectIntervalItem = ({ interval: EntInterval; isSelected: boolean; }) => { + const { item } = useProductItemContext(); const [hover, setHover] = useState(false); return ( @@ -108,21 +204,11 @@ const SelectIntervalItem = ({ className="group flex items-center justify-between w-full" onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} - endComponent={ - // isSelected && - hover && ( - - ) - } > -
- {getIntervalText(interval)} +
+ + {getIntervalText({ interval, intervalCount: item?.interval_count })} +
); diff --git a/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx index 784befefc..d49ce4425 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx @@ -10,6 +10,8 @@ import { import { BillingInterval, UsageModel } from "@autumn/shared"; import { useProductItemContext } from "../../../ProductItemContext"; import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; +import { CustomiseIntervalPopover } from "../CusomiseIntervalPopover"; +import { formatIntervalText } from "@/utils/formatUtils/formatTextUtils"; export const SelectCycle = () => { const { item, setItem } = useProductItemContext(); @@ -27,13 +29,13 @@ export const SelectCycle = () => { }); }; - const intervalText = (interval: BillingInterval) => { - return interval === BillingInterval.SemiAnnual - ? "per half year" - : interval === BillingInterval.OneOff - ? "one off" - : `per ${interval}`; - }; + // const intervalText = (interval: BillingInterval) => { + // return interval === BillingInterval.SemiAnnual + // ? "per half year" + // : interval === BillingInterval.OneOff + // ? "one off" + // : `per ${interval}`; + // }; return (
@@ -45,7 +47,37 @@ export const SelectCycle = () => { period. - {/* + +
+
+ +
+
+
+ ); +}; + +{ + /* @@ -67,28 +99,5 @@ export const SelectCycle = () => { )} - */} - -
-
- -
-
-
- ); -}; + */ +} From 0877ebee855c477b4a7e43a6c2fee39c169a77d7 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 7 Aug 2025 08:06:47 -0700 Subject: [PATCH 24/37] fix: rolled back create product item config --- .../views/onboarding2/integrate/AITools.tsx | 2 +- .../onboarding2/model-pricing/EditProduct.tsx | 6 +- .../model-pricing/ModelPricing.tsx | 5 +- .../product-item/CreateProductItem.tsx | 24 +++- .../product-item/CreateProductItem2.tsx | 16 ++- .../CreateItemDialogContent.tsx | 123 +++++++----------- .../product-item-config/FeatureItemConfig.tsx | 43 ++++-- .../product/product-item/useSteps.tsx | 1 + 8 files changed, 119 insertions(+), 101 deletions(-) diff --git a/vite/src/views/onboarding2/integrate/AITools.tsx b/vite/src/views/onboarding2/integrate/AITools.tsx index c25582fe6..7dcc76097 100644 --- a/vite/src/views/onboarding2/integrate/AITools.tsx +++ b/vite/src/views/onboarding2/integrate/AITools.tsx @@ -24,7 +24,7 @@ export const AITools = () => { // Base64 encode the configuration for Cursor's install URL const encodedConfig = btoa(JSON.stringify(mcpConfig)); - const cursorInstallUrl = `https://cursor.com/install-mcp?name=autumn&config=${encodedConfig}`; + const cursorInstallUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=Autumn%20Docs&config=${encodedConfig}`; // Manual JSON configuration for copy-paste const manualConfig = JSON.stringify( diff --git a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx index b170ba6eb..419671735 100644 --- a/vite/src/views/onboarding2/model-pricing/EditProduct.tsx +++ b/vite/src/views/onboarding2/model-pricing/EditProduct.tsx @@ -162,8 +162,10 @@ export const EditProduct = ({ mutate }: { mutate: any }) => { <> {product.items.length == 0 ? (

- Next, add items to define what customers with this product - get access to, and how much they should be charged for it. + {/* Next, add items to define what customers with this product + get access to, and how much they should be charged for it. */} + Next, add which features your customers can use on this + product and how much it should cost.

) : (

Create your products

-

+

To start, model your app's pricing by creating a product for - your free plans,
paid plans and any add-ons or - top-ups. + your free plans, paid plans and any add-ons or top-ups.

{firstItemCreated && ( diff --git a/vite/src/views/products/product/product-item/CreateProductItem.tsx b/vite/src/views/products/product/product-item/CreateProductItem.tsx index d828f40b2..0a54c2221 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem.tsx @@ -1,7 +1,7 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogTrigger } from "@/components/ui/dialog"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { ProductItemContext } from "./ProductItemContext"; import { @@ -17,6 +17,7 @@ import { CreateItemDialogContent } from "./create-product-item/CreateItemDialogC import { Plus } from "lucide-react"; import { useSteps } from "./useSteps"; import { CreateItemStep } from "./utils/CreateItemStep"; +import { isFeatureItem } from "@/utils/product/getItemType"; export const defaultProductItem: ProductItem = { feature_id: null, @@ -89,9 +90,15 @@ export function CreateProductItem() { }; const stepState = useSteps({ - initialStep: CreateItemStep.SelectItemType, + initialStep: CreateItemStep.CreateItem, }); + useEffect(() => { + if (open && isFeatureItem(item) && features.length == 0) { + stepState.replaceStep(CreateItemStep.CreateFeature); + } + }, [open]); + return ( + + +
diff --git a/vite/src/views/products/product/product-item/CreateProductItem2.tsx b/vite/src/views/products/product/product-item/CreateProductItem2.tsx index b9782dec4..59f77a036 100644 --- a/vite/src/views/products/product/product-item/CreateProductItem2.tsx +++ b/vite/src/views/products/product/product-item/CreateProductItem2.tsx @@ -18,6 +18,7 @@ import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelP import { useSteps } from "./useSteps"; import { CreateItemStep } from "./utils/CreateItemStep"; import { cn } from "@/lib/utils"; +import { defaultPriceItem } from "./create-product-item/defaultItemConfigs"; const defaultProductItem: ProductItem = { feature_id: null, @@ -96,14 +97,25 @@ export function CreateProductItem2({ }} > -
+
+ + +
diff --git a/vite/src/views/products/product/product-item/create-product-item/CreateItemDialogContent.tsx b/vite/src/views/products/product/product-item/create-product-item/CreateItemDialogContent.tsx index 5c9774e8f..92045d266 100644 --- a/vite/src/views/products/product/product-item/create-product-item/CreateItemDialogContent.tsx +++ b/vite/src/views/products/product/product-item/create-product-item/CreateItemDialogContent.tsx @@ -37,8 +37,15 @@ export const CreateItemDialogContent = ({ const { features, setFeatures } = useProductContext(); const { stepState, item, setItem } = useProductItemContext(); - const { stepVal, popStep, pushStep, resetSteps, previousStep, replaceStep } = - stepState; + const { + stepVal, + popStep, + pushStep, + resetSteps, + previousStep, + replaceStep, + stepCount, + } = stepState; useEffect(() => { if (open) { @@ -46,60 +53,17 @@ export const CreateItemDialogContent = ({ } }, [open]); - const getTabValue = () => { - return getItemType(item); - }; - - const handleTabChange = (value: string) => { - if (value === ProductItemType.Feature) { - setItem({ - ...item, - feature_id: item.feature_id, - price: null, - tiers: null, - }); - } - - if (value === ProductItemType.FeaturePrice) { - const feature = getFeature(item.feature_id, features); - if (!feature || feature?.type === FeatureType.Boolean) { - setItem(defaultPaidFeatureItem); - } else { - const newIncludedUsage = - item.included_usage == Infinite ? 0 : item.included_usage; - - let newInterval = item.interval; - if ( - notNullish(item.interval) && - !Object.values(BillingInterval).includes(item.interval) - ) { - newInterval = BillingInterval.Month; - } - - setItem({ - ...item, - included_usage: newIncludedUsage, - interval: newInterval, - tiers: [{ to: Infinite, amount: 0 }], - }); - } - } - - if (value === ProductItemType.Price) { - setItem(defaultPriceItem); - } - }; - const handleFeatureCreated = async (feature: CreateFeatureType) => { setFeatures([...features, feature]); setItem({ ...item, feature_id: feature.id! }); - // replaceStep(CreateItemStep.CreateItem); - if (previousStep === CreateItemStep.CreateItem) { - replaceStep(CreateItemStep.CreateItem); - } else { - pushStep(CreateItemStep.CreateItem); - } + // // replaceStep(CreateItemStep.CreateItem); + // if (previousStep === CreateItemStep.CreateItem) { + // replaceStep(CreateItemStep.CreateItem); + // } else { + // pushStep(CreateItemStep.CreateItem); + // } + replaceStep(CreateItemStep.CreateItem); }; const tabTriggerClass = @@ -108,26 +72,26 @@ export const CreateItemDialogContent = ({ const itemType = getItemType(item); return ( - {stepVal === CreateItemStep.SelectItemType ? ( - - ) : stepVal === CreateItemStep.CreateFeature ? ( - popStep()} - /> - ) : stepVal === CreateItemStep.SelectFeature ? ( - - ) : ( - <> - -
- - Add {keyToTitle(itemType)} Item - + { + // stepVal === CreateItemStep.SelectItemType ? ( + // + // ) : + stepVal === CreateItemStep.CreateFeature ? ( + 1 ? popStep : undefined} + /> + ) : ( + <> + +
+ + Add {keyToTitle(itemType)} + - {/* + {/* Feature @@ -147,15 +111,18 @@ export const CreateItemDialogContent = ({ */} -
- +
+ +
-
-
+ - popStep()} /> - - )} + 1 ? popStep : undefined} + /> + + ) + } ); }; diff --git a/vite/src/views/products/product/product-item/product-item-config/FeatureItemConfig.tsx b/vite/src/views/products/product/product-item/product-item-config/FeatureItemConfig.tsx index 2c11456da..40fba5dbf 100644 --- a/vite/src/views/products/product/product-item/product-item-config/FeatureItemConfig.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/FeatureItemConfig.tsx @@ -1,5 +1,5 @@ import { useProductItemContext } from "../ProductItemContext"; -import { BillingInterval, Infinite } from "@autumn/shared"; +import { BillingInterval, FeatureUsageType, Infinite } from "@autumn/shared"; import { SelectCycle } from "./components/feature-price/SelectBillingCycle"; import { IncludedUsage } from "./components/IncludedUsage"; import { SelectResetCycle } from "./components/SelectResetCycle"; @@ -11,8 +11,14 @@ import { PlusIcon } from "lucide-react"; import { PrepaidToggle } from "./components/feature-price/PrepaidToggle"; import { AdvancedItemConfig } from "./advanced-config/AdvancedItemConfig"; import { notNullish } from "@/utils/genUtils"; +import { + getFeature, + getFeatureUsageType, +} from "@/utils/product/entitlementUtils"; +import { useProductContext } from "../../ProductContext"; export const FeatureConfig = () => { + const { features } = useProductContext(); const { item, setItem } = useProductItemContext(); if (!item.feature_id) return null; @@ -40,6 +46,13 @@ export const FeatureConfig = () => { }); }; + const price = + getFeatureUsageType({ item, features }) == FeatureUsageType.Continuous + ? "10" + : "1"; + + const feature = getFeature(item?.feature_id, features); + return ( <>
@@ -64,18 +77,24 @@ export const FeatureConfig = () => { )} - {/* {isFeature && ( -
- + {isFeature && ( +
+

+ If you want to charge for usage of this feature (eg. ${price} per{" "} + {feature?.display?.singular ?? feature?.name ?? "feature"}) +

+
+ +
- )} */} + )} ); diff --git a/vite/src/views/products/product/product-item/useSteps.tsx b/vite/src/views/products/product/product-item/useSteps.tsx index 8daa927bd..0caa353ab 100644 --- a/vite/src/views/products/product/product-item/useSteps.tsx +++ b/vite/src/views/products/product/product-item/useSteps.tsx @@ -52,5 +52,6 @@ export const useSteps = ({ initialStep }: { initialStep: CreateItemStep }) => { resetSteps, replaceStep, previousStep: stepStack.length > 1 ? stepStack[stepStack.length - 2] : null, + stepCount: stepStack.length, }; }; From 3ef62d952a89bffcabf0c61379f9ac019f48f428 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 7 Aug 2025 08:10:52 -0700 Subject: [PATCH 25/37] fix: description on create product item --- .../products/product/product-item/ProductItemTable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vite/src/views/products/product/product-item/ProductItemTable.tsx b/vite/src/views/products/product/product-item/ProductItemTable.tsx index 546ece242..b337a1b3b 100644 --- a/vite/src/views/products/product/product-item/ProductItemTable.tsx +++ b/vite/src/views/products/product/product-item/ProductItemTable.tsx @@ -206,11 +206,11 @@ export const ProductItemTable = () => { isOnboarding && "px-2" )} > -

+ {/*

Product items determine what customers get access to and how they're billed. Start by adding one. -

- {/*

+

*/} +

Product items determine what customers get access to and how they're billed{" "} { Priced Features:{" "} features that have a price based on usage (eg, $1 per credit)

-
*/} +
)}
From 9235b0239b8ecd91bc23eba7aacc8d6783502267 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 7 Aug 2025 11:46:20 -0700 Subject: [PATCH 26/37] converting interval to intervalSets --- server/src/external/stripe/stripeSubUtils.ts | 27 +- .../stripe/stripeSubUtils/createStripeSub.ts | 1 + .../stripeSubUtils/getStripeSubItems.ts | 13 +- .../getStripeSubItems/getArrearItems.ts | 11 +- server/src/external/stripe/utils.ts | 75 ++- .../src/internal/api/entitled/checkUtils.ts | 3 +- .../add-product/handleCreateCheckout.ts | 7 +- .../addProductFlow/handlePaidProduct.ts | 22 +- .../scheduleFlow/handleScheduleFunction.ts | 8 +- .../createUsageInvoiceItems.ts | 19 +- .../upgradeDiffIntFlow/updateCurSchedules.ts | 19 +- .../upgradeSameIntFlow/updateSubsSameInt.ts | 27 +- .../attach/attachUtils/attachUtils.ts | 12 +- .../attach/attachUtils/getAttachConfig.ts | 51 +- .../createContUseInvoiceItems.ts | 31 +- .../getContUseItems/getContUseInvoiceItems.ts | 19 +- .../updateStripeSub/updateStripeSub.ts | 16 +- .../attach/checkout/previewToCheckoutRes.ts | 4 - .../getNewProductPreview.ts | 54 +- .../getUpdateQuantityPreview.ts | 220 ------- .../getUpgradeProductPreview.ts | 35 +- .../change-product/billRemainingUsages.ts | 474 +++++++-------- .../handleDowngrade/cancelCurSubs.ts | 11 +- .../customers/change-product/handleUpgrade.ts | 569 ------------------ .../customers/change-product/scheduleUtils.ts | 35 +- .../cusFeatureResponseUtils/getCusBalances.ts | 7 +- .../previewItemUtils/getItemsForNewProduct.ts | 28 +- .../handlers/handleListProductsBeta.ts | 2 - .../updateProductDetails.ts | 14 +- .../internal/products/pricecn/pricecnUtils.ts | 7 +- .../products/prices/billingIntervalUtils.ts | 77 ++- .../prices/priceUtils/constructPriceUtils.ts | 4 +- .../prices/priceUtils/convertPrice.ts | 26 +- .../prices/priceUtils/priceIntervalUtils.ts | 177 +++++- .../getProductItemDisplay.ts | 46 +- .../getProductResponse.ts | 15 +- server/tests/utils/stripeUtils.ts | 12 +- .../testProductUtils/testProductUtils.ts | 7 +- shared/index.ts | 1 + .../cusResModels/cusFeatureResponse.ts | 3 + .../prodItemResponseModels.ts | 1 + shared/utils/intervalUtils.ts | 43 ++ shared/utils/productDisplayUtils.ts | 35 +- vite/src/utils/product/priceUtils.ts | 32 +- .../product/product-item/formatProductItem.ts | 37 +- .../product/prices/CreateFixedPrice.tsx | 18 +- .../products/product/prices/PricingConfig.tsx | 15 +- 47 files changed, 1066 insertions(+), 1304 deletions(-) delete mode 100644 server/src/internal/customers/attach/handleAttachPreview/getUpdateQuantityPreview.ts delete mode 100644 server/src/internal/customers/change-product/handleUpgrade.ts create mode 100644 shared/utils/intervalUtils.ts diff --git a/server/src/external/stripe/stripeSubUtils.ts b/server/src/external/stripe/stripeSubUtils.ts index 1fa8390e8..d265f0991 100644 --- a/server/src/external/stripe/stripeSubUtils.ts +++ b/server/src/external/stripe/stripeSubUtils.ts @@ -45,7 +45,7 @@ export const getStripeSubs = async ({ } catch (error: any) { console.log( `(warning) getStripeSubs: Failed to get sub ${subId}`, - error.message, + error.message ); return null; } @@ -133,7 +133,7 @@ export const getUsageBasedSub = async ({ let autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id); if (autumnSub) { let containsFeature = autumnSub.usage_features.includes( - feature.internal_id!, + feature.internal_id! ); if (containsFeature) { return stripeSub; @@ -149,7 +149,7 @@ export const getUsageBasedSub = async ({ if ( !usageFeatures || usageFeatures.find( - (feat: any) => feat.internal_id == feature.internal_id, + (feat: any) => feat.internal_id == feature.internal_id ) === undefined ) { continue; @@ -179,15 +179,14 @@ export const getSubItemsForCusProduct = async ({ prices.some( (p) => p.config?.stripe_price_id == item.price.id || - (p.config as UsagePriceConfig).stripe_product_id == - item.price.product, + (p.config as UsagePriceConfig).stripe_product_id == item.price.product ) ) { subItems.push(item); } } let otherSubItems = stripeSub.items.data.filter( - (item) => !subItems.some((i) => i.id == item.id), + (item) => !subItems.some((i) => i.id == item.id) ); return { subItems, otherSubItems }; @@ -216,12 +215,17 @@ export const getStripeSchedules = async ({ } const prices = await Promise.all(batchPricesGet); const interval = prices[0].recurring?.interval; - const billingInterval = stripeToAutumnInterval({ - interval: prices[0].recurring?.interval as string, - intervalCount: prices[0].recurring?.interval_count || 1, - }); + // const billingInterval = stripeToAutumnInterval({ + // interval: prices[0].recurring?.interval as string, + // intervalCount: prices[0].recurring?.interval_count || 1, + // }); - return { schedule, interval: billingInterval, prices }; + return { + schedule, + interval: interval as BillingInterval, + intervalCount: prices[0].recurring?.interval_count || 1, + prices, + }; } catch (error: any) { console.log("Error getting stripe schedule.", error.message); return null; @@ -237,6 +241,7 @@ export const getStripeSchedules = async ({ return schedulesAndSubs.filter((schedule) => schedule !== null) as { schedule: Stripe.SubscriptionSchedule; interval: BillingInterval; + intervalCount: number; prices: Stripe.Price[]; }[]; }; diff --git a/server/src/external/stripe/stripeSubUtils/createStripeSub.ts b/server/src/external/stripe/stripeSubUtils/createStripeSub.ts index cc314ca1f..e9608e5af 100644 --- a/server/src/external/stripe/stripeSubUtils/createStripeSub.ts +++ b/server/src/external/stripe/stripeSubUtils/createStripeSub.ts @@ -58,6 +58,7 @@ export const createStripeSub = async ({ ? getAlignedIntervalUnix({ alignWithUnix: anchorToUnix, interval: itemSet.interval, + intervalCount: itemSet.intervalCount, now, }) : undefined; diff --git a/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts b/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts index 3fd7c5603..2a0723d86 100644 --- a/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts +++ b/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts @@ -194,7 +194,18 @@ export const getStripeSubItems = async ({ }); } - itemSets.sort((a, b) => compareBillingIntervals(a.interval, b.interval)); + itemSets.sort((a, b) => + compareBillingIntervals({ + configA: { + interval: a.interval, + intervalCount: a.intervalCount, + }, + configB: { + interval: b.interval, + intervalCount: b.intervalCount, + }, + }) + ); return itemSets; }; diff --git a/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts b/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts index 1193bbbec..475e84887 100644 --- a/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts +++ b/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts @@ -2,6 +2,7 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { BillingInterval, BillingType, + intervalsDifferent, Organization, UsagePriceConfig, } from "@autumn/shared"; @@ -23,7 +24,15 @@ export const getArrearItems = ({ let placeholderItems: any[] = []; for (const price of prices) { let billingType = getBillingType(price.config!); - if (price.config!.interval! != interval) { + if ( + intervalsDifferent({ + intervalA: { + interval: price.config!.interval!, + intervalCount: price.config!.interval_count!, + }, + intervalB: { interval, intervalCount }, + }) + ) { continue; } diff --git a/server/src/external/stripe/utils.ts b/server/src/external/stripe/utils.ts index 834fa9d2c..1b8ea3c48 100644 --- a/server/src/external/stripe/utils.ts +++ b/server/src/external/stripe/utils.ts @@ -77,44 +77,55 @@ export const calculateMetered1Price = ({ export const subToAutumnInterval = (sub: Stripe.Subscription) => { let recuringItem = sub.items.data.find((i) => i.price.recurring != null); if (!recuringItem) { - return BillingInterval.OneOff; + return { + interval: BillingInterval.OneOff, + intervalCount: 1, + }; } - return stripeToAutumnInterval({ - interval: recuringItem.price.recurring!.interval, - intervalCount: recuringItem.price.recurring!.interval_count, - }); + return { + interval: recuringItem.price.recurring!.interval as BillingInterval, + intervalCount: recuringItem.price.recurring!.interval_count || 1, + }; + // return stripeToAutumnInterval({ + // interval: recuringItem.price.recurring!.interval, + // intervalCount: recuringItem.price.recurring!.interval_count, + // }); }; -export const stripeToAutumnInterval = ({ - interval, - intervalCount, -}: { - interval: string; - intervalCount: number; -}) => { - if (interval === "month" && intervalCount === 1) { - return BillingInterval.Month; - } +// export const stripeToAutumnInterval = ({ +// interval, +// intervalCount, +// }: { +// interval: string; +// intervalCount: number; +// }) => { +// if (interval === "month" && intervalCount === 1) { +// return BillingInterval.Month; +// } - if (interval === "month" && intervalCount === 3) { - return BillingInterval.Quarter; - } +// // if (interval === "month" && intervalCount === 3) { +// // return BillingInterval.Quarter; +// // } - if (interval === "month" && intervalCount === 6) { - return BillingInterval.SemiAnnual; - } +// // if (interval === "month" && intervalCount === 6) { +// // return BillingInterval.SemiAnnual; +// // } - if ( - (interval === "month" && intervalCount === 12) || - (interval === "year" && intervalCount === 1) - ) { - return BillingInterval.Year; - } -}; +// if ( +// (interval === "month" && intervalCount === 12) || +// (interval === "year" && intervalCount === 1) +// ) { +// return BillingInterval.Year; +// } +// }; export const subItemToAutumnInterval = (item: Stripe.SubscriptionItem) => { - return stripeToAutumnInterval({ - interval: item.price.recurring?.interval!, - intervalCount: item.price.recurring?.interval_count!, - }); + return { + interval: item.price.recurring?.interval as BillingInterval, + intervalCount: item.price.recurring?.interval_count || 1, + }; + // return stripeToAutumnInterval({ + // interval: item.price.recurring?.interval!, + // intervalCount: item.price.recurring?.interval_count!, + // }); }; diff --git a/server/src/internal/api/entitled/checkUtils.ts b/server/src/internal/api/entitled/checkUtils.ts index 318a87b35..00b519df5 100644 --- a/server/src/internal/api/entitled/checkUtils.ts +++ b/server/src/internal/api/entitled/checkUtils.ts @@ -118,6 +118,7 @@ export const getOptions = ({ anchorToUnix, proration, interval: (i.interval || BillingInterval.OneOff) as BillingInterval, + intervalCount: i.interval_count || 1, now, }); @@ -139,7 +140,7 @@ export const getOptions = ({ } const currentOptions = cusProduct?.options.find( - (o) => o.feature_id == i.feature_id, + (o) => o.feature_id == i.feature_id ); let currentQuantity = currentOptions?.quantity; diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index 6900aa9d5..fe9c5f659 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -61,7 +61,12 @@ export const handleCreateCheckout = async ({ }); let billingCycleAnchorUnixSeconds = org.config.anchor_start_of_month - ? Math.floor(getNextStartOfMonthUnix(itemSets[0].interval) / 1000) + ? Math.floor( + getNextStartOfMonthUnix({ + interval: itemSets[0].interval, + intervalCount: itemSets[0].intervalCount, + }) / 1000 + ) : undefined; if (attachParams.billingAnchor) { diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts index 938d04e12..d5714254e 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts @@ -20,6 +20,7 @@ import { AttachScenario, BillingInterval, ErrCode, + intervalsDifferent, SuccessCode, } from "@autumn/shared"; import Stripe from "stripe"; @@ -78,15 +79,28 @@ export const handlePaidProduct = async ({ continue; } - let mergeWithSub = mergeSubs.find( - (sub) => subToAutumnInterval(sub) == itemSet.interval - ); + let mergeWithSub = mergeSubs.find((sub) => { + let subInterval = subToAutumnInterval(sub); + return !intervalsDifferent({ + intervalA: { + interval: subInterval.interval, + intervalCount: subInterval.intervalCount, + }, + intervalB: { + interval: itemSet.interval, + intervalCount: itemSet.intervalCount, + }, + }); + }); let subscription; try { let billingCycleAnchorUnix; if (org.config.anchor_start_of_month) { - billingCycleAnchorUnix = getNextStartOfMonthUnix(itemSet.interval); + billingCycleAnchorUnix = getNextStartOfMonthUnix({ + interval: itemSet.interval, + intervalCount: itemSet.intervalCount, + }); } if (attachParams.billingAnchor) { diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts index c7b1d25d4..c87046602 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts @@ -69,7 +69,9 @@ export const handleScheduleFunction = async ({ const stripeSchedules: Stripe.SubscriptionSchedule[] = []; for (const itemSet of itemSets) { let scheduleObj = schedules.find( - (schedule) => schedule.interval === itemSet.interval, + (schedule) => + schedule.interval === itemSet.interval && + schedule.intervalCount === itemSet.intervalCount ); // If schedule exists, update it @@ -110,7 +112,7 @@ export const handleScheduleFunction = async ({ cusProductId: curCusProduct.id, updates: { scheduled_ids: curCusProduct.scheduled_ids?.filter( - (id) => !newScheduledIds.includes(id), + (id) => !newScheduledIds.includes(id) ), }, }); @@ -147,7 +149,7 @@ export const handleScheduleFunction = async ({ product_ids: [product.id], customer_id: attachParams.customer.id || attachParams.customer.internal_id, - }), + }) ); } else { res.status(200).json({ diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts index e347396ce..bc7234914 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts @@ -17,6 +17,7 @@ import { UsagePriceConfig, BillingType, BillingInterval, + intervalsDifferent, } from "@autumn/shared"; import Stripe from "stripe"; @@ -28,6 +29,7 @@ export const getUsageInvoiceItems = async ({ cusProduct, stripeSubs, interval, + intervalCount, }: { db: DrizzleCli; logger: any; @@ -35,6 +37,7 @@ export const getUsageInvoiceItems = async ({ cusProduct: FullCusProduct; stripeSubs: Stripe.Subscription[]; interval?: BillingInterval; + intervalCount?: number; }) => { const { stripeCli, org } = attachParams; @@ -72,7 +75,14 @@ export const getUsageInvoiceItems = async ({ }); if (!sub) continue; - if (interval && interval !== subToAutumnInterval(sub)) continue; + if ( + interval && + intervalsDifferent({ + intervalA: { interval, intervalCount }, + intervalB: subToAutumnInterval(sub), + }) + ) + continue; cusEntIds.push(cusEnt.id); @@ -106,6 +116,7 @@ export const createUsageInvoiceItems = async ({ invoiceId, logger, interval, + intervalCount, }: { db: DrizzleCli; attachParams: AttachParams; @@ -114,6 +125,7 @@ export const createUsageInvoiceItems = async ({ invoiceId?: string; logger: any; interval?: BillingInterval; + intervalCount?: number; }) => { const { stripeCli } = attachParams; @@ -123,6 +135,7 @@ export const createUsageInvoiceItems = async ({ cusProduct, stripeSubs, interval, + intervalCount, logger, }); @@ -131,7 +144,7 @@ export const createUsageInvoiceItems = async ({ const invoiceItem = invoiceItems[i]; const createInvoiceItem = async () => { logger.info( - `🌟 Creating usage invoice item: ${invoiceItem.description}, amount: ${invoiceItem.price_data.unit_amount}`, + `🌟 Creating usage invoice item: ${invoiceItem.description}, amount: ${invoiceItem.price_data.unit_amount}` ); await stripeCli.invoiceItems.create({ @@ -171,7 +184,7 @@ export const resetUsageBalances = async ({ }); let index = cusProduct.customer_entitlements.findIndex( - (ce) => ce.id === cusEntId, + (ce) => ce.id === cusEntId ); cusProduct.customer_entitlements[index] = { diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/updateCurSchedules.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/updateCurSchedules.ts index aac780950..049f4ce6f 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/updateCurSchedules.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/updateCurSchedules.ts @@ -4,7 +4,11 @@ import { updateScheduledSubWithNewItems } from "@/internal/customers/change-prod import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { ItemSet } from "@/utils/models/ItemSet.js"; import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; -import { FullCusProduct } from "@autumn/shared"; +import { + FullCusProduct, + intervalsDifferent, + intervalsSame, +} from "@autumn/shared"; import Stripe from "stripe"; import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js"; @@ -39,7 +43,7 @@ export const updateCurSchedules = async ({ }); for (const scheduleObj of schedules) { - const { interval, schedule } = scheduleObj; + const { interval, intervalCount, schedule } = scheduleObj; // If schedule has passed, skip this step. let phase = schedule.phases.length > 0 ? schedule.phases[0] : null; @@ -51,7 +55,16 @@ export const updateCurSchedules = async ({ } // Get corresponding item set - const itemSet = itemSets.find((itemSet) => itemSet.interval === interval); + + const itemSet = itemSets.find((itemSet) => + intervalsSame({ + intervalA: { interval, intervalCount }, + intervalB: { + interval: itemSet.interval, + intervalCount: itemSet.intervalCount, + }, + }) + ); if (!itemSet) { continue; } diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/updateSubsSameInt.ts b/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/updateSubsSameInt.ts index 8f35a26b5..070298c2b 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/updateSubsSameInt.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/updateSubsSameInt.ts @@ -3,7 +3,12 @@ import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSub import { subToAutumnInterval } from "@/external/stripe/utils.js"; import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AttachConfig, FullCusProduct, Replaceable } from "@autumn/shared"; +import { + AttachConfig, + FullCusProduct, + intervalsSame, + Replaceable, +} from "@autumn/shared"; import { addSubItemsToRemove } from "../attachFuncUtils.js"; import { updateStripeSub } from "../../attachUtils/updateStripeSub/updateStripeSub.js"; import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; @@ -51,9 +56,16 @@ export const updateSubsByInt = async ({ // const replaceables: Replaceable[] = []; for (const sub of stripeSubs) { - let interval = subToAutumnInterval(sub); + // let interval = subToAutumnInterval(sub); + + let subInterval = subToAutumnInterval(sub); + let itemSet = itemSets.find((itemSet) => { + return intervalsSame({ + intervalA: itemSet, + intervalB: subInterval, + }); + })!; - let itemSet = itemSets.find((itemSet) => itemSet.interval === interval)!; await addSubItemsToRemove({ sub, cusProduct: curCusProduct, @@ -67,14 +79,17 @@ export const updateSubsByInt = async ({ stripeSubs: [sub], itemSet, logger, - interval, + interval: itemSet.interval, + intervalCount: itemSet.intervalCount, }); if (latestInvoice) { invoices.push(latestInvoice); } - logger.info(`Updated sub ${sub.id}, interval ${interval}`); + logger.info( + `Updated sub ${sub.id}, interval ${itemSet.interval}, intervalCount ${itemSet.intervalCount}` + ); } const batchInvUpdate = []; @@ -85,7 +100,7 @@ export const updateSubsByInt = async ({ attachParams, stripeInvoice: invoice, logger, - }), + }) ); } diff --git a/server/src/internal/customers/attach/attachUtils/attachUtils.ts b/server/src/internal/customers/attach/attachUtils/attachUtils.ts index d2dc8999c..cdf0c631e 100644 --- a/server/src/internal/customers/attach/attachUtils/attachUtils.ts +++ b/server/src/internal/customers/attach/attachUtils/attachUtils.ts @@ -1,5 +1,8 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { getFirstInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; +import { + getLargestInterval, + intervalsDifferent, +} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; import { subToAutumnInterval } from "@/external/stripe/utils.js"; import Stripe from "stripe"; import { attachParamsToProduct } from "./convertAttachParams.js"; @@ -12,7 +15,10 @@ export const getCycleWillReset = ({ stripeSubs: Stripe.Subscription[]; }) => { const product = attachParamsToProduct({ attachParams }); - const firstInterval = getFirstInterval({ prices: product.prices }); + const firstInterval = getLargestInterval({ prices: product.prices }); const prevInterval = subToAutumnInterval(stripeSubs[0]); - return prevInterval !== firstInterval; + return intervalsDifferent({ + intervalA: firstInterval, + intervalB: prevInterval, + }); }; diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts index 26543571b..856659ed6 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts @@ -1,6 +1,6 @@ import { AttachParams } from "../../cusProducts/AttachParams.js"; import { AttachFlags } from "../models/AttachFlags.js"; -import { AttachConfig, AttachBranch } from "@autumn/shared"; +import { AttachConfig, AttachBranch, intervalsSame } from "@autumn/shared"; import { AttachBody } from "@autumn/shared"; import { isFreeProduct } from "@/internal/products/productUtils.js"; import { nullish } from "@/utils/genUtils.js"; @@ -27,12 +27,49 @@ export const intervalsAreSame = ({ let newProduct = attachParamsToProduct({ attachParams }); let curPrices = cusProductToPrices({ cusProduct: curCusProduct! }); - let curIntervals = new Set(curPrices.map((p) => p.config.interval)); - let newIntervals = new Set(newProduct.prices.map((p) => p.config.interval)); - return ( - curIntervals.size === newIntervals.size && - [...curIntervals].every((interval) => newIntervals.has(interval)) - ); + for (const price of curPrices) { + let hasSimilarInterval = newProduct.prices.some((p) => { + return intervalsSame({ + intervalA: price.config, + intervalB: p.config, + }); + }); + + if (!hasSimilarInterval) { + return false; + } + } + + for (const price of newProduct.prices) { + let hasSimilarInterval = curPrices.some((p) => { + return intervalsSame({ + intervalA: price.config, + intervalB: p.config, + }); + }); + + if (!hasSimilarInterval) { + return false; + } + } + + return true; + // let curIntervals = new Set( + // curPrices.map((p) => ({ + // interval: p.config.interval, + // intervalCount: p.config.interval_count, + // })) + // ); + // let newIntervals = new Set( + // newProduct.prices.map((p) => ({ + // interval: p.config.interval, + // intervalCount: p.config.interval_count, + // })) + // ); + // return ( + // curIntervals.size === newIntervals.size && + // [...curIntervals].every((interval) => newIntervals.has(interval)) + // ); }; export const getAttachConfig = async ({ diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts index 5c33127f3..28b6c55e4 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts @@ -4,6 +4,8 @@ import { BillingType, FullCusProduct, FullProduct, + intervalsDifferent, + intervalsSame, } from "@autumn/shared"; import Stripe from "stripe"; import { getContUseInvoiceItems } from "./getContUseInvoiceItems.js"; @@ -35,7 +37,7 @@ export const filterContUsageProrations = async ({ subscription: sub.id, }); - const interval = subToAutumnInterval(sub); + const intervalSet = subToAutumnInterval(sub); for (const item of upcomingLines.data) { if (!item.proration) continue; @@ -48,12 +50,12 @@ export const filterContUsageProrations = async ({ if (!price) continue; logger.info( - `Deleting ii: ${item.description} - ${item.amount / 100} (${interval})`, + `Deleting ii: ${item.description} - ${item.amount / 100} (${intervalSet.interval}, ${intervalSet.intervalCount})` ); await stripeCli.invoiceItems.del( // @ts-ignore -- Stripe types are not correct - item.parent.subscription_item_details.invoice_item, + item.parent.subscription_item_details.invoice_item ); } }; @@ -64,12 +66,14 @@ export const createAndFilterContUseItems = async ({ stripeSubs, logger, interval, + intervalCount, }: { attachParams: AttachParams; curMainProduct: FullCusProduct; stripeSubs: Stripe.Subscription[]; logger: any; interval?: BillingInterval; + intervalCount?: number; }) => { const { stripeCli, customer, org } = attachParams; const product = attachParamsToProduct({ attachParams }); @@ -88,8 +92,13 @@ export const createAndFilterContUseItems = async ({ }); let sub = - stripeSubs.find((sub) => subToAutumnInterval(sub) == interval) || - stripeSubs[0]; + stripeSubs.find((sub) => { + return intervalsSame({ + intervalA: { interval: interval!, intervalCount: intervalCount! }, + intervalB: subToAutumnInterval(sub), + }); + // subToAutumnInterval(sub) == interval + }) || stripeSubs[0]; await filterContUsageProrations({ sub, @@ -113,12 +122,20 @@ export const createAndFilterContUseItems = async ({ product.prices.find((p) => p.id === item.price_id) || curPrices.find((p) => p.id === item.price_id); - if (interval && price?.config.interval !== interval) { + if ( + interval && + price?.config && + intervalsDifferent({ + // price?.config.interval !== interval + intervalA: price?.config, + intervalB: { interval, intervalCount }, + }) + ) { continue; } logger.info( - `Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`, + `Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}` ); await stripeCli.invoiceItems.create({ diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts index 87cfa2b7b..b52756774 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts @@ -7,6 +7,7 @@ import { FullCustomerEntitlement, FullEntitlement, getFeatureInvoiceDescription, + intervalsSame, PreviewLineItem, Price, } from "@autumn/shared"; @@ -121,7 +122,7 @@ export const getContUseInvoiceItems = async ({ const cusEnts = cusProduct ? cusProduct.customer_entitlements : []; const product = attachParamsToProduct({ attachParams }); - const intervalsSame = intervalsAreSame({ attachParams }); + const allIntervalsSame = intervalsAreSame({ attachParams }); const curItems = stripeSubs ? await getCurContUseItems({ stripeSubs, @@ -150,7 +151,7 @@ export const getContUseInvoiceItems = async ({ ? getRelatedCusPrice(prevCusEnt, cusPrices)! : undefined; - if (!intervalsSame || !prevCusEnt || !stripeSubs) { + if (!allIntervalsSame || !prevCusEnt || !stripeSubs) { const newItem = await getContUseNewItems({ price, ent, @@ -159,7 +160,7 @@ export const getContUseInvoiceItems = async ({ }); const prevItem = curItems.find( - (item) => item.price_id === prevCusPrice?.price.id, + (item) => item.price_id === prevCusPrice?.price.id ); newItems.push(newItem); @@ -172,12 +173,16 @@ export const getContUseInvoiceItems = async ({ } const curItem = curItems.find( - (item) => item.price_id === prevCusPrice?.price.id, + (item) => item.price_id === prevCusPrice?.price.id ); - let sub = stripeSubs!.find( - (sub) => subToAutumnInterval(sub) === price.config.interval, - ); + let sub = stripeSubs!.find((sub) => { + let subInterval = subToAutumnInterval(sub); + return intervalsSame({ + intervalA: price.config, + intervalB: subInterval, + }); + }); let { oldItem, diff --git a/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts b/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts index 23819390f..111bafe71 100644 --- a/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts +++ b/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts @@ -1,5 +1,5 @@ import Stripe from "stripe"; -import { BillingInterval, AttachConfig } from "@autumn/shared"; +import { BillingInterval, AttachConfig, intervalsSame } from "@autumn/shared"; import { ProrationBehavior } from "@autumn/shared"; import { SubService } from "@/internal/subscriptions/SubService.js"; import { ItemSet } from "@/utils/models/ItemSet.js"; @@ -23,7 +23,7 @@ export const getSubAndInvoiceItems = async ({ let subItems = items.filter( (i: any, index: number) => - i.deleted || prices[index].config!.interval !== BillingInterval.OneOff, + i.deleted || prices[index].config!.interval !== BillingInterval.OneOff ); let addInvoiceItems = items.filter((i: any, index: number) => { @@ -48,6 +48,7 @@ export const updateStripeSub = async ({ itemSet, logger, interval, + intervalCount, }: { db: DrizzleCli; attachParams: AttachParams; @@ -58,6 +59,7 @@ export const updateStripeSub = async ({ shouldPreview?: boolean; logger: any; interval?: BillingInterval; + intervalCount?: number; }) => { const { curMainProduct } = attachParamToCusProducts({ attachParams }); const { stripeCli, customer, org, paymentMethod } = attachParams; @@ -69,7 +71,13 @@ export const updateStripeSub = async ({ const curSub = (interval - ? stripeSubs.find((s) => subToAutumnInterval(s) === interval) + ? stripeSubs.find((s) => { + let subInterval = subToAutumnInterval(s); + return intervalsSame({ + intervalA: { interval, intervalCount }, + intervalB: subInterval, + }); + }) : stripeSubs[0]) || stripeSubs[0]; // 1. Update subscription @@ -110,6 +118,7 @@ export const updateStripeSub = async ({ stripeSubs, logger, interval: config.sameIntervals ? interval : undefined, + intervalCount: config.sameIntervals ? intervalCount : undefined, }); // 3. Create prorations for continuous use items @@ -118,6 +127,7 @@ export const updateStripeSub = async ({ curMainProduct: curMainProduct!, stripeSubs, interval, + intervalCount, logger, }); diff --git a/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts b/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts index 322eb1a0a..f8747f73b 100644 --- a/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts +++ b/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts @@ -108,10 +108,6 @@ export const previewToCheckoutRes = async ({ return acc; } - // if (item.interval !== newProduct.properties?.interval_group) { - // return acc; - // } - if (isPriceItem(item)) { return acc.plus(item.price || 0); } diff --git a/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts index c5745205a..42dc67ea7 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts @@ -10,15 +10,10 @@ import { getNextStartOfMonthUnix, } from "@/internal/products/prices/billingIntervalUtils.js"; import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -import { getLastInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; -import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; +import { isFreeProduct } from "@/internal/products/productUtils.js"; import { getMergeCusProduct } from "../attachFunctions/addProductFlow/getMergeCusProduct.js"; -import { - formatUnixToDate, - formatUnixToDateTime, - notNullish, - nullish, -} from "@/utils/genUtils.js"; +import { notNullish, nullish } from "@/utils/genUtils.js"; export const getNewProductPreview = async ({ branch, @@ -38,7 +33,10 @@ export const getNewProductPreview = async ({ let anchorToUnix = undefined; if (org.config.anchor_start_of_month) { - anchorToUnix = getNextStartOfMonthUnix(BillingInterval.Month); + anchorToUnix = getNextStartOfMonthUnix({ + interval: BillingInterval.Month, + intervalCount: 1, + }); } const { mergeCusProduct, mergeSubs } = await getMergeCusProduct({ @@ -77,12 +75,16 @@ export const getNewProductPreview = async ({ config, }); - let minInterval = getLastInterval({ + // let minInterval = getLastInterval({ + // prices: newProduct.prices, + // ents: newProduct.entitlements, + // }); + let min = getSmallestInterval({ prices: newProduct.prices, ents: newProduct.entitlements, }); - let getAligned = notNullish(anchorToUnix) && notNullish(minInterval); + let getAligned = notNullish(anchorToUnix) && notNullish(min); let dueAt = freeTrial ? freeTrialToStripeTimestamp({ @@ -92,11 +94,16 @@ export const getNewProductPreview = async ({ : getAligned ? getAlignedIntervalUnix({ alignWithUnix: anchorToUnix!, - interval: minInterval, + interval: min!.interval, + intervalCount: min!.intervalCount, now: attachParams.now, }) - : notNullish(minInterval) - ? addBillingIntervalUnix(attachParams.now || Date.now(), minInterval) + : notNullish(min) + ? addBillingIntervalUnix({ + unixTimestamp: attachParams.now || Date.now(), + interval: min!.interval, + intervalCount: min!.intervalCount, + }) : undefined; dueNextCycle = !nullish(dueAt) @@ -125,18 +132,25 @@ export const getNewProductPreview = async ({ // Next cycle at if (!dueNextCycle) { if (!isFreeProduct(newProduct.prices) && branch != AttachBranch.OneOff) { - let minInterval = getLastInterval({ prices: newProduct.prices }); + let min = getSmallestInterval({ + prices: newProduct.prices, + ents: newProduct.entitlements, + }); dueNextCycle = { line_items: items.filter((item) => { let price = newProduct.prices.find( (price) => price.id == item.price_id ); - return price?.config.interval == minInterval; + return ( + price?.config.interval == min!.interval && + (price?.config.interval_count || 1) == (min!.intervalCount || 1) + ); + }), + due_at: addBillingIntervalUnix({ + unixTimestamp: attachParams.now || Date.now(), + interval: min!.interval, + intervalCount: min!.intervalCount, }), - due_at: addBillingIntervalUnix( - attachParams.now || Date.now(), - minInterval - ), }; } } diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpdateQuantityPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpdateQuantityPreview.ts deleted file mode 100644 index f8f295bf9..000000000 --- a/server/src/internal/customers/attach/handleAttachPreview/getUpdateQuantityPreview.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; -import { getOptions } from "@/internal/api/entitled/checkUtils.js"; -import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js"; -import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js"; -import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -import { - addBillingIntervalUnix, - getAlignedIntervalUnix, -} from "@/internal/products/prices/billingIntervalUtils.js"; -import { getLastInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; -import { isFreeProduct } from "@/internal/products/productUtils.js"; -import { mapToProductItems } from "@/internal/products/productV2Utils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - AttachBranch, - BillingInterval, - FreeTrial, - PreviewLineItem, - Price, - UsageModel, -} from "@autumn/shared"; - -import Stripe from "stripe"; -import { - attachParamToCusProducts, - attachParamsToProduct, -} from "../attachUtils/convertAttachParams.js"; -import { intervalsAreSame } from "../attachUtils/getAttachConfig.js"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { Decimal } from "decimal.js"; - -const getNextCycleAt = ({ - prices, - stripeSubs, - willCycleReset, - interval, - now, - freeTrial, -}: { - prices: Price[]; - stripeSubs: Stripe.Subscription[]; - willCycleReset: boolean; - interval: BillingInterval; - now?: number; - freeTrial?: FreeTrial | null; -}) => { - now = now || Date.now(); - - if (freeTrial) { - return { - next_cycle_at: - freeTrialToStripeTimestamp({ - freeTrial, - now, - })! * 1000, - }; - } - - if (willCycleReset) { - const minInterval = getLastInterval({ prices }); - return { - next_cycle_at: addBillingIntervalUnix(now, minInterval), - }; - } - - const minInterval = getLastInterval({ prices }); - const nextCycleAt = getAlignedIntervalUnix({ - alignWithUnix: stripeSubs[0].current_period_end * 1000, - interval: minInterval, - alwaysReturn: true, - }); - - return { - next_cycle_at: nextCycleAt, - }; -}; - -export const getUpdateQuantityPreview = async ({ - req, - attachParams, - branch, - now, -}: { - req: ExtendedRequest; - attachParams: AttachParams; - branch: AttachBranch; - now: number; -}) => { - const { logtail: logger } = req; - - const { stripeCli } = attachParams; - - const { curMainProduct, curSameProduct } = attachParamToCusProducts({ - attachParams, - }); - const curCusProduct = curMainProduct!; - - const stripeSubs = await getStripeSubs({ - stripeCli, - subIds: curCusProduct?.subscription_ids || [], - expand: ["items.data.price.tiers"], - }); - - const curPreviewItems = await getItemsForCurProduct({ - stripeSubs, - attachParams, - now, - logger, - }); - - // Get prorated amounts for new product - const newProduct = attachParamsToProduct({ attachParams }); - const intervalsSame = intervalsAreSame({ attachParams }); - const anchorToUnix = - intervalsSame && stripeSubs.length > 0 - ? stripeSubs[0].current_period_end * 1000 - : undefined; - - const newPreviewItems = await getItemsForNewProduct({ - newProduct, - attachParams, - now, - anchorToUnix, - freeTrial: attachParams.freeTrial, - stripeSubs, - logger, - }); - - const lastInterval = getLastInterval({ prices: newProduct.prices }); - - let dueNextCycle = undefined; - if (!isFreeProduct(newProduct.prices)) { - const nextCycleAt = getNextCycleAt({ - prices: newProduct.prices, - stripeSubs, - willCycleReset: !intervalsSame, - interval: lastInterval, - now, - freeTrial: attachParams.freeTrial, - }); - - let nextCycleItems = await getItemsForNewProduct({ - newProduct, - attachParams, - interval: attachParams.freeTrial ? undefined : lastInterval, - logger, - }); - - dueNextCycle = { - line_items: nextCycleItems, - due_at: nextCycleAt.next_cycle_at, - }; - } - - let items = [...curPreviewItems, ...newPreviewItems]; - - for (const item of structuredClone(curPreviewItems)) { - let priceId = item.price_id; - let newItem = newPreviewItems.find((i) => i.price_id == priceId); - - if (!newItem) { - continue; - } - - let newItemAmount = new Decimal(newItem?.amount ?? 0).toDecimalPlaces(2); - let curItemAmount = new Decimal(item.amount ?? 0).toDecimalPlaces(2); - - if (newItemAmount.add(curItemAmount).eq(0)) { - items = items.filter((i) => i.price_id !== priceId); - } - } - - const dueTodayAmt = items - .reduce((acc, item) => acc.plus(item.amount ?? 0), new Decimal(0)) - .toDecimalPlaces(2) - .toNumber(); - - let options = getOptions({ - prodItems: mapToProductItems({ - prices: newProduct.prices, - entitlements: newProduct.entitlements, - features: attachParams.features, - }), - features: attachParams.features, - anchorToUnix, - now, - freeTrial: attachParams.freeTrial, - cusProduct: curSameProduct, - }); - - items = items.filter((item) => item.amount !== 0); - - if (branch == AttachBranch.UpdatePrepaidQuantity) { - items = items.filter((item) => item.usage_model == UsageModel.Prepaid); - dueNextCycle!.line_items = dueNextCycle!.line_items.filter( - (item) => item.usage_model == UsageModel.Prepaid, - ); - } - - let dueToday: - | { - line_items: PreviewLineItem[]; - total: number; - } - | undefined = { - line_items: items, - total: dueTodayAmt, - }; - - if (branch == AttachBranch.SameCustomEnts) { - dueToday = undefined; - } - - return { - currency: attachParams.org.default_currency, - due_today: dueToday, - due_next_cycle: dueNextCycle, - options, - }; -}; diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts index e704578c0..62d5611ce 100644 --- a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts +++ b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts @@ -5,7 +5,7 @@ import { } from "../attachUtils/convertAttachParams.js"; import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; -import { getFirstInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; +import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js"; import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js"; import { getOptions } from "@/internal/api/entitled/checkUtils.js"; @@ -29,13 +29,12 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free import { Decimal } from "decimal.js"; import { intervalsAreSame } from "../attachUtils/getAttachConfig.js"; import { isFreeProduct } from "@/internal/products/productUtils.js"; -import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js"; +import { formatUnixToDateTime, notNullish, nullish } from "@/utils/genUtils.js"; const getNextCycleAt = ({ prices, stripeSubs, willCycleReset, - interval, now, freeTrial, branch, @@ -44,7 +43,6 @@ const getNextCycleAt = ({ prices: Price[]; stripeSubs: Stripe.Subscription[]; willCycleReset: boolean; - interval: BillingInterval; now?: number; freeTrial?: FreeTrial | null; branch: AttachBranch; @@ -68,17 +66,28 @@ const getNextCycleAt = ({ }; } - if (willCycleReset) { - const firstInterval = getFirstInterval({ prices }); + const firstInterval = getLargestInterval({ prices }); + + if (nullish(firstInterval)) { return { - next_cycle_at: addBillingIntervalUnix(now, firstInterval), + next_cycle_at: now, + }; + } + + if (willCycleReset) { + return { + next_cycle_at: addBillingIntervalUnix({ + unixTimestamp: now, + interval: firstInterval!.interval, + intervalCount: firstInterval!.intervalCount, + }), }; } - const firstInterval = getFirstInterval({ prices }); const nextCycleAt = getAlignedIntervalUnix({ alignWithUnix: stripeSubs[0].current_period_end * 1000, - interval: firstInterval, + interval: firstInterval!.interval, + intervalCount: firstInterval!.intervalCount, alwaysReturn: true, now, }); @@ -154,7 +163,7 @@ export const getUpgradeProductPreview = async ({ config, }); - const lastInterval = getFirstInterval({ prices: newProduct.prices }); + const largestInterval = getLargestInterval({ prices: newProduct.prices }); let dueNextCycle = undefined; if (!isFreeProduct(newProduct.prices)) { @@ -162,7 +171,6 @@ export const getUpgradeProductPreview = async ({ prices: newProduct.prices, stripeSubs, willCycleReset: !intervalsSame, - interval: lastInterval, now, freeTrial: attachParams.freeTrial, branch, @@ -172,7 +180,10 @@ export const getUpgradeProductPreview = async ({ let nextCycleItems = await getItemsForNewProduct({ newProduct, attachParams, - interval: attachParams.freeTrial ? undefined : lastInterval, + interval: attachParams.freeTrial ? undefined : largestInterval?.interval, + intervalCount: attachParams.freeTrial + ? undefined + : largestInterval?.intervalCount, logger, withPrepaid, branch, diff --git a/server/src/internal/customers/change-product/billRemainingUsages.ts b/server/src/internal/customers/change-product/billRemainingUsages.ts index 89fa17441..ae69888b5 100644 --- a/server/src/internal/customers/change-product/billRemainingUsages.ts +++ b/server/src/internal/customers/change-product/billRemainingUsages.ts @@ -29,275 +29,275 @@ import { import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js"; // Add usage to end of cycle -const addUsageToNextInvoice = async ({ - db, - intervalToInvoiceItems, - intervalToSub, - customer, - org, - logger, - attachParams, -}: { - db: DrizzleCli; - intervalToInvoiceItems: any; - intervalToSub: any; - customer: any; - org: any; - logger: any; - attachParams: AttachParams; -}) => { - for (const interval in intervalToInvoiceItems) { - const itemsToInvoice = intervalToInvoiceItems[interval]; +// const addUsageToNextInvoice = async ({ +// db, +// intervalToInvoiceItems, +// intervalToSub, +// customer, +// org, +// logger, +// attachParams, +// }: { +// db: DrizzleCli; +// intervalToInvoiceItems: any; +// intervalToSub: any; +// customer: any; +// org: any; +// logger: any; +// attachParams: AttachParams; +// }) => { +// for (const interval in intervalToInvoiceItems) { +// const itemsToInvoice = intervalToInvoiceItems[interval]; - if (itemsToInvoice.length === 0) { - continue; - } +// if (itemsToInvoice.length === 0) { +// continue; +// } - // Add items to invoice - const stripeCli = createStripeCli({ - org: org, - env: customer.env, - }); +// // Add items to invoice +// const stripeCli = createStripeCli({ +// org: org, +// env: customer.env, +// }); - for (const item of itemsToInvoice) { - const { amount, description } = item; +// for (const item of itemsToInvoice) { +// const { amount, description } = item; - logger.info( - ` feature: ${item.feature.id}, overage: ${item.overage}, amount: ${amount}`, - ); +// logger.info( +// ` feature: ${item.feature.id}, overage: ${item.overage}, amount: ${amount}`, +// ); - let relatedSub = intervalToSub[interval]; - if (!relatedSub) { - continue; - } +// let relatedSub = intervalToSub[interval]; +// if (!relatedSub) { +// continue; +// } - // Create invoice item - let invoiceItem = { - customer: customer.processor.id, - currency: org.default_currency, - description, - price_data: { - product: (item.price.config! as UsagePriceConfig).stripe_product_id!, - unit_amount: Math.round(amount * 100), - currency: org.default_currency, - }, - subscription: relatedSub.id, - period: { - start: item.periodStart, - end: item.periodEnd, - }, - }; +// // Create invoice item +// let invoiceItem = { +// customer: customer.processor.id, +// currency: org.default_currency, +// description, +// price_data: { +// product: (item.price.config! as UsagePriceConfig).stripe_product_id!, +// unit_amount: Math.round(amount * 100), +// currency: org.default_currency, +// }, +// subscription: relatedSub.id, +// period: { +// start: item.periodStart, +// end: item.periodEnd, +// }, +// }; - await stripeCli.invoiceItems.create(invoiceItem); +// await stripeCli.invoiceItems.create(invoiceItem); - // Update cus ent to 0 - await CusEntService.update({ - db, - id: item.relatedCusEnt!.id, - updates: getResetBalancesUpdate({ - cusEnt: item.relatedCusEnt!, - allowance: 0, - }), - }); +// // Update cus ent to 0 +// await CusEntService.update({ +// db, +// id: item.relatedCusEnt!.id, +// updates: getResetBalancesUpdate({ +// cusEnt: item.relatedCusEnt!, +// allowance: 0, +// }), +// }); - // Update existing cusEnt in attachParams - let cusProducts = attachParams.cusProducts; - for (const cusProduct of cusProducts!) { - for (let i = 0; i < cusProduct.customer_entitlements.length; i++) { - let cusEnt = cusProduct.customer_entitlements[i]; - if (cusEnt.id === item.relatedCusEnt!.id) { - let balancesUpdate = getResetBalancesUpdate({ - cusEnt, - allowance: 0, - }); - cusProduct.customer_entitlements[i] = { - ...cusEnt, - ...balancesUpdate, - }; - } - } - } - } - } -}; +// // Update existing cusEnt in attachParams +// let cusProducts = attachParams.cusProducts; +// for (const cusProduct of cusProducts!) { +// for (let i = 0; i < cusProduct.customer_entitlements.length; i++) { +// let cusEnt = cusProduct.customer_entitlements[i]; +// if (cusEnt.id === item.relatedCusEnt!.id) { +// let balancesUpdate = getResetBalancesUpdate({ +// cusEnt, +// allowance: 0, +// }); +// cusProduct.customer_entitlements[i] = { +// ...cusEnt, +// ...balancesUpdate, +// }; +// } +// } +// } +// } +// } +// }; -const invoiceForUsageImmediately = async ({ - db, - intervalToInvoiceItems, - customer, - org, - logger, - curCusProduct, - attachParams, - newSubs, -}: { - db: DrizzleCli; - intervalToInvoiceItems: any; - customer: any; - org: any; - logger: any; - curCusProduct: FullCusProduct; - attachParams: AttachParams; - newSubs: Stripe.Subscription[]; -}) => { - // 1. Create invoice - const stripeCli = createStripeCli({ - org: org, - env: customer.env, - }); - const product = curCusProduct.product; +// const invoiceForUsageImmediately = async ({ +// db, +// intervalToInvoiceItems, +// customer, +// org, +// logger, +// curCusProduct, +// attachParams, +// newSubs, +// }: { +// db: DrizzleCli; +// intervalToInvoiceItems: any; +// customer: any; +// org: any; +// logger: any; +// curCusProduct: FullCusProduct; +// attachParams: AttachParams; +// newSubs: Stripe.Subscription[]; +// }) => { +// // 1. Create invoice +// const stripeCli = createStripeCli({ +// org: org, +// env: customer.env, +// }); +// const product = curCusProduct.product; - let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; - if (invoiceItems.length === 0) { - return; - } +// let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; +// if (invoiceItems.length === 0) { +// return; +// } - let invoice: Stripe.Invoice; - let newInvoice = false; +// let invoice: Stripe.Invoice; +// let newInvoice = false; - if (attachParams.invoiceOnly && newSubs.length > 0) { - invoice = await stripeCli.invoices.retrieve( - newSubs[0].latest_invoice as string, - ); +// if (attachParams.invoiceOnly && newSubs.length > 0) { +// invoice = await stripeCli.invoices.retrieve( +// newSubs[0].latest_invoice as string, +// ); - if (invoice.status !== "draft") { - newInvoice = true; - invoice = await stripeCli.invoices.create({ - customer: customer.processor.id, - auto_advance: true, - }); - } - } else { - newInvoice = true; +// if (invoice.status !== "draft") { +// newInvoice = true; +// invoice = await stripeCli.invoices.create({ +// customer: customer.processor.id, +// auto_advance: true, +// }); +// } +// } else { +// newInvoice = true; - invoice = await stripeCli.invoices.create({ - customer: customer.processor.id, - auto_advance: true, - }); - } +// invoice = await stripeCli.invoices.create({ +// customer: customer.processor.id, +// auto_advance: true, +// }); +// } - let autumnInvoiceItems: InvoiceItem[] = []; +// let autumnInvoiceItems: InvoiceItem[] = []; - for (const item of invoiceItems) { - // const amount = getPriceForOverage(item.price, item.overage); - const { amount, description } = item; - let config = item.price.config! as UsagePriceConfig; - // let stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!); - let stripeProdId = config.stripe_product_id; - if (!stripeProdId) { - try { - let stripePrice = await stripeCli.prices.retrieve( - config.stripe_price_id!, - ); - stripeProdId = stripePrice.product as string; - } catch (error) {} - } +// for (const item of invoiceItems) { +// // const amount = getPriceForOverage(item.price, item.overage); +// const { amount, description } = item; +// let config = item.price.config! as UsagePriceConfig; +// // let stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!); +// let stripeProdId = config.stripe_product_id; +// if (!stripeProdId) { +// try { +// let stripePrice = await stripeCli.prices.retrieve( +// config.stripe_price_id!, +// ); +// stripeProdId = stripePrice.product as string; +// } catch (error) {} +// } - if (!stripeProdId) { - stripeProdId = product.processor?.id; - } +// if (!stripeProdId) { +// stripeProdId = product.processor?.id; +// } - logger.info( - `🌟🌟🌟 (Bill remaining) created invoice item: ${description} -- ${amount}`, - ); +// logger.info( +// `🌟🌟🌟 (Bill remaining) created invoice item: ${description} -- ${amount}`, +// ); - let invoiceItem = { - customer: customer.processor.id, - invoice: invoice.id, - currency: org.default_currency, - description, - price_data: { - product: stripeProdId!, - unit_amount: Math.round(amount * 100), - currency: org.default_currency, - }, - period: { - start: item.periodStart, - end: item.periodEnd, - }, - }; +// let invoiceItem = { +// customer: customer.processor.id, +// invoice: invoice.id, +// currency: org.default_currency, +// description, +// price_data: { +// product: stripeProdId!, +// unit_amount: Math.round(amount * 100), +// currency: org.default_currency, +// }, +// period: { +// start: item.periodStart, +// end: item.periodEnd, +// }, +// }; - let stripeInvoiceItem = await stripeCli.invoiceItems.create(invoiceItem); +// let stripeInvoiceItem = await stripeCli.invoiceItems.create(invoiceItem); - autumnInvoiceItems.push({ - price_id: item.price.id!, - internal_feature_id: item.feature.internal_id || null, - description: description, - period_start: item.periodStart * 1000, - period_end: item.periodEnd * 1000, - stripe_id: stripeInvoiceItem.id, - }); +// autumnInvoiceItems.push({ +// price_id: item.price.id!, +// internal_feature_id: item.feature.internal_id || null, +// description: description, +// period_start: item.periodStart * 1000, +// period_end: item.periodEnd * 1000, +// stripe_id: stripeInvoiceItem.id, +// }); - await CusEntService.update({ - db, - id: item.relatedCusEnt!.id, - updates: { - balance: 0, - }, - }); - let index = curCusProduct.customer_entitlements.findIndex( - (ce) => ce.id === item.relatedCusEnt!.id, - ); +// await CusEntService.update({ +// db, +// id: item.relatedCusEnt!.id, +// updates: { +// balance: 0, +// }, +// }); +// let index = curCusProduct.customer_entitlements.findIndex( +// (ce) => ce.id === item.relatedCusEnt!.id, +// ); - curCusProduct.customer_entitlements[index] = { - ...curCusProduct.customer_entitlements[index], - balance: 0, - }; - } +// curCusProduct.customer_entitlements[index] = { +// ...curCusProduct.customer_entitlements[index], +// balance: 0, +// }; +// } - if (newInvoice) { - await stripeCli.invoices.finalizeInvoice(invoice.id); +// if (newInvoice) { +// await stripeCli.invoices.finalizeInvoice(invoice.id); - const { paid, error } = await payForInvoice({ - stripeCli, - paymentMethod: null, - invoiceId: invoice.id, - logger, - }); +// const { paid, error } = await payForInvoice({ +// stripeCli, +// paymentMethod: null, +// invoiceId: invoice.id, +// logger, +// }); - if (!paid) { - logger.warn("Failed to pay invoice for remaining usages", { - stripeInvoice: newInvoice, - paymentError: error, - }); - } - } +// if (!paid) { +// logger.warn("Failed to pay invoice for remaining usages", { +// stripeInvoice: newInvoice, +// paymentError: error, +// }); +// } +// } - await insertInvoiceFromAttach({ - db, - attachParams, - invoiceId: invoice.id, - logger, - }); -}; +// await insertInvoiceFromAttach({ +// db, +// attachParams, +// invoiceId: invoice.id, +// logger, +// }); +// }; -const getRemainingUsagesPreview = async ({ - intervalToInvoiceItems, - curCusProduct, -}: { - intervalToInvoiceItems: any; - curCusProduct: FullCusProduct; -}) => { - let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; - if (invoiceItems.length === 0) { - return; - } +// const getRemainingUsagesPreview = async ({ +// intervalToInvoiceItems, +// curCusProduct, +// }: { +// intervalToInvoiceItems: any; +// curCusProduct: FullCusProduct; +// }) => { +// let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; +// if (invoiceItems.length === 0) { +// return; +// } - let items = []; - for (const item of invoiceItems) { - const amount = getPriceForOverage(item.price, item.overage); - const description = `${curCusProduct.product.name} - ${ - item.feature.name - } x ${Math.round(item.usage)}`; +// let items = []; +// for (const item of invoiceItems) { +// const amount = getPriceForOverage(item.price, item.overage); +// const description = `${curCusProduct.product.name} - ${ +// item.feature.name +// } x ${Math.round(item.usage)}`; - items.push({ - amount, - description, - }); - } +// items.push({ +// amount, +// description, +// }); +// } - return items; -}; +// return items; +// }; // export const billForRemainingUsages = async ({ // db, diff --git a/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts b/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts index 0cfd905c0..8ade0b0e0 100644 --- a/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts +++ b/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts @@ -1,4 +1,9 @@ import { getSubItemsForCusProduct } from "@/external/stripe/stripeSubUtils.js"; +import { subToAutumnInterval } from "@/external/stripe/utils.js"; +import { + priceToIntervalKey, + toIntervalKey, +} from "@/internal/products/prices/priceUtils/convertPrice.js"; import { notNullish } from "@/utils/genUtils.js"; import { FullCusProduct } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; @@ -32,8 +37,10 @@ export const cancelCurSubs = async ({ cusProduct: curCusProduct, }); - let interval = sub.items.data[0].price.recurring!.interval; - intervalToOtherSubs[interval] = { + // let interval = sub.items.data[0].price.recurring!.interval; + let subInterval = subToAutumnInterval(sub); + let intervalKey = toIntervalKey(subInterval); + intervalToOtherSubs[intervalKey] = { otherSubItems, otherSub: sub, }; diff --git a/server/src/internal/customers/change-product/handleUpgrade.ts b/server/src/internal/customers/change-product/handleUpgrade.ts deleted file mode 100644 index fb818416b..000000000 --- a/server/src/internal/customers/change-product/handleUpgrade.ts +++ /dev/null @@ -1,569 +0,0 @@ -// import { getStripeExpandedInvoice } from "@/external/stripe/stripeInvoiceUtils.js"; -// import { -// getStripeSchedules, -// getStripeSubs, -// } from "@/external/stripe/stripeSubUtils.js"; -// import { createStripeCli } from "@/external/stripe/utils.js"; - -// import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -// import { -// attachToInsertParams, -// isFreeProduct, -// } from "@/internal/products/productUtils.js"; -// import RecaseError from "@/utils/errorUtils.js"; -// import { -// FullCusProduct, -// ErrCode, -// FullProduct, -// CusProductStatus, -// APIVersion, -// UsagePriceConfig, -// AttachScenario, -// } from "@autumn/shared"; - -// import { StatusCodes } from "http-status-codes"; -// import Stripe from "stripe"; -// import { createFullCusProduct } from "../add-product/createFullCusProduct.js"; -// import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js"; -// import { -// AttachParams, -// AttachResultSchema, -// } from "../cusProducts/AttachParams.js"; -// import { CusProductService } from "../cusProducts/CusProductService.js"; -// import { -// getInvoiceItems, -// insertInvoiceFromAttach, -// } from "@/internal/invoices/invoiceUtils.js"; -// import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; -// import { updateScheduledSubWithNewItems } from "./scheduleUtils/updateScheduleWithNewItems.js"; - -// // import { billForRemainingUsages } from "./billRemainingUsages.js"; -// import { updateStripeSubscription } from "@/external/stripe/stripeSubUtils/updateStripeSub.js"; -// import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js"; - -// import { -// addBillingIntervalUnix, -// subtractBillingIntervalUnix, -// } from "@/internal/products/prices/billingIntervalUtils.js"; - -// import { differenceInSeconds } from "date-fns"; -// import { SuccessCode } from "@autumn/shared"; -// import { notNullish } from "@/utils/genUtils.js"; -// import { DrizzleCli } from "@/db/initDrizzle.js"; -// import { ExtendedRequest } from "@/utils/models/Request.js"; -// import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; - -// export enum ProrationBehavior { -// Immediately = "immediately", -// NextBilling = "next_billing", -// None = "none", -// } - -// // UPGRADE FUNCTIONS -// export const handleStripeSubUpdate = async ({ -// db, -// stripeCli, -// curCusProduct, -// attachParams, -// disableFreeTrial, -// stripeSubs, -// logger, -// carryExistingUsages = false, -// prorationBehavior = ProrationBehavior.Immediately, -// shouldPreview = false, -// }: { -// db: DrizzleCli; -// stripeCli: Stripe; -// curCusProduct: FullCusProduct; -// attachParams: AttachParams; -// disableFreeTrial?: boolean; -// stripeSubs: Stripe.Subscription[]; -// logger: any; -// carryExistingUsages?: boolean; -// prorationBehavior?: ProrationBehavior; -// shouldPreview?: boolean; -// }) => { -// // HANLDE UPGRADE - -// // 1. Get item sets -// const itemSets = await getStripeSubItems({ -// attachParams, -// carryExistingUsages, -// }); - -// const firstSub = stripeSubs[0]; -// const firstItemSet = itemSets[0]; -// let curPrices = curCusProduct.customer_prices.map((cp) => cp.price); - -// // 1. DELETE ITEMS FROM CURRENT SUB THAT CORRESPOND TO OLD PRODUCT -// for (const item of firstSub.items.data) { -// let stripePriceExists = curPrices.some( -// (p) => -// p.config!.stripe_price_id === item.price.id || -// (p.config as UsagePriceConfig).stripe_product_id === item.price.product, -// ); - -// let stripeProdExists = -// item.price.product == curCusProduct.product.processor?.id; - -// if (!stripePriceExists && !stripeProdExists) { -// continue; -// } - -// firstItemSet.items.push({ -// id: item.id, -// deleted: true, -// }); -// } - -// // 2. Add trial to new subscription? -// let trialEnd; -// if (!disableFreeTrial) { -// trialEnd = freeTrialToStripeTimestamp({ -// freeTrial: attachParams.freeTrial, -// now: attachParams.now, -// }); -// } - -// // 3. Update current subscription -// let newSubs = []; -// const subUpdateRes = await updateStripeSubscription({ -// db, -// stripeCli, -// subscriptionId: firstSub.id, -// trialEnd, -// org: attachParams.org, -// customer: attachParams.customer, -// invoiceOnly: attachParams.invoiceOnly || false, -// prorationBehavior, -// logger, -// itemSet: firstItemSet, -// shouldPreview, -// }); - -// if (shouldPreview) { -// return subUpdateRes; -// } - -// let subUpdate = subUpdateRes as Stripe.Subscription; - -// newSubs.push(subUpdate); - -// // 4. If scheduled_ids exist, need to update schedule too (BRUH)! -// if (curCusProduct.scheduled_ids && curCusProduct.scheduled_ids.length > 0) { -// let schedules = await getStripeSchedules({ -// stripeCli, -// scheduleIds: curCusProduct.scheduled_ids, -// }); - -// for (const scheduleObj of schedules) { -// const { interval, schedule } = scheduleObj; - -// // If schedule has passed, skip this step. -// let phase = schedule.phases.length > 0 ? schedule.phases[0] : null; -// let now = Date.now(); -// if (schedule.test_clock) { -// let testClock = await stripeCli.testHelpers.testClocks.retrieve( -// schedule.test_clock as string, -// ); -// now = testClock.frozen_time * 1000; -// } - -// if (phase && phase.start_date * 1000 < now) { -// logger.info("Note: Schedule has passed, skipping"); -// continue; -// } - -// // Get corresponding item set -// const itemSet = itemSets.find((itemSet) => itemSet.interval === interval); -// if (!itemSet) { -// continue; -// } - -// await updateScheduledSubWithNewItems({ -// scheduleObj, -// newItems: itemSet.items, -// stripeCli, -// cusProductsForGroup: [curCusProduct], -// itemSet, -// db, -// org: attachParams.org, -// env: attachParams.customer.env, -// }); -// } -// } - -// // 5. Insert invoice -// await insertInvoiceFromAttach({ -// db, -// attachParams, -// invoiceId: subUpdate.latest_invoice as string, -// logger, -// }); - -// // 2. Create new subscriptions -// let newSubIds = []; -// newSubIds.push(firstSub.id); -// const newItemSets = itemSets.slice(1); -// let invoiceIds = []; - -// // CREATE NEW SUBSCRIPTIONS -// for (const itemSet of newItemSets) { -// // 1. Next billing date for first sub -// // const nextCycleAnchor = firstSub.current_period_end * 1000; -// const nextCycleAnchor = subUpdate.current_period_end * 1000; -// let nextCycleAnchorUnix = nextCycleAnchor; -// const naturalBillingDate = addBillingIntervalUnix( -// Date.now(), -// itemSet.interval, -// ); - -// while (true) { -// const subtractedUnix = subtractBillingIntervalUnix( -// nextCycleAnchorUnix, -// itemSet.interval, -// ); - -// if (subtractedUnix < Date.now()) { -// break; -// } - -// nextCycleAnchorUnix = subtractedUnix; -// } - -// let billingCycleAnchorUnix: number | undefined = nextCycleAnchorUnix; -// if ( -// differenceInSeconds( -// new Date(naturalBillingDate), -// new Date(nextCycleAnchorUnix), -// ) < 60 -// ) { -// billingCycleAnchorUnix = undefined; -// } - -// const newSub = (await createStripeSub({ -// db, -// stripeCli, -// customer: attachParams.customer, -// org: attachParams.org, -// itemSet, -// invoiceOnly: attachParams.invoiceOnly || false, -// freeTrial: attachParams.freeTrial, -// anchorToUnix: billingCycleAnchorUnix, -// })) as Stripe.Subscription; - -// newSubs.push(newSub); -// newSubIds.push(newSub.id); -// invoiceIds.push(newSub.latest_invoice as string); -// } - -// // 3. Cancel old subscriptions -// let remainingExistingSubIds = stripeSubs.slice(1).map((sub) => sub.id); - -// return { -// subUpdate, -// newSubIds, -// invoiceIds, -// remainingExistingSubIds, -// newSubs, -// }; -// }; - -// const handleOnlyEntsChanged = async ({ -// req, -// res, -// attachParams, -// curCusProduct, -// carryExistingUsages = false, -// }: { -// req: any; -// res: any; -// attachParams: AttachParams; -// curCusProduct: FullCusProduct; -// carryExistingUsages?: boolean; -// }) => { -// const logger = req.logtail; -// logger.info("Only entitlements changed, no need to update prices"); - -// // Remove subscription from previous cus product -// await CusProductService.update({ -// db: req.db, -// cusProductId: curCusProduct.id, -// updates: { -// subscription_ids: [], -// }, -// }); - -// await createFullCusProduct({ -// db: req.db, -// attachParams: attachToInsertParams(attachParams, attachParams.products[0]), -// subscriptionIds: curCusProduct.subscription_ids || [], -// disableFreeTrial: false, -// keepResetIntervals: true, -// carryExistingUsages, -// logger, -// }); - -// logger.info("✅ Successfully updated entitlements for product"); - -// let org = attachParams.org; - -// let apiVersion = org.api_version || APIVersion.v1; -// if (apiVersion >= APIVersion.v1_1) { -// res.status(200).json( -// AttachResultSchema.parse({ -// customer_id: attachParams.customer.id, -// product_ids: attachParams.products.map((p) => p.id), -// code: SuccessCode.FeaturesUpdated, -// message: `Successfully updated features for customer ${attachParams.customer.id} on product ${attachParams.products[0].name}`, -// }), -// ); -// } else { -// res.status(200).json({ -// success: true, -// message: `Successfully updated entitlements for ${curCusProduct.product.name}`, -// }); -// } -// }; - -// export const handleUpgrade = async ({ -// req, -// res, -// attachParams, -// curCusProduct, -// curFullProduct, -// hasPricesChanged = true, -// fromReq = true, -// carryExistingUsages = false, -// prorationBehavior, -// newVersion = false, -// updateSameProduct = false, -// }: { -// req: ExtendedRequest; -// res: any; -// attachParams: AttachParams; -// curCusProduct: FullCusProduct; -// curFullProduct: FullProduct; -// hasPricesChanged?: boolean; -// fromReq?: boolean; -// carryExistingUsages?: boolean; -// prorationBehavior?: ProrationBehavior; -// newVersion?: boolean; -// updateSameProduct?: boolean; -// }) => { -// const logger = req.logtail; -// const { org, customer, products } = attachParams; -// let product = products[0]; - -// let disableFreeTrial = false; -// if (newVersion) { -// disableFreeTrial = true; -// } - -// if (!hasPricesChanged) { -// await handleOnlyEntsChanged({ -// req, -// res, -// attachParams, -// curCusProduct, -// carryExistingUsages, -// }); -// return; -// } - -// logger.info( -// `Upgrading ${curFullProduct.name} to ${product.name} for ${customer.id}`, -// ); - -// const stripeCli = createStripeCli({ org, env: customer.env }); -// const stripeSubs = await getStripeSubs({ -// stripeCli, -// subIds: curCusProduct.subscription_ids, -// }); - -// // 1. If current product has trial and new product has trial, cancel and start new subscription -// let trialToTrial = -// curCusProduct.trial_ends_at && -// curCusProduct.trial_ends_at > Date.now() && -// attachParams.freeTrial && -// !disableFreeTrial; - -// let trialToPaid = -// curCusProduct.trial_ends_at && -// curCusProduct.trial_ends_at > Date.now() && -// !attachParams.freeTrial && -// !newVersion; // If trial to paid and not migrating, cancel trial and start new sub immediately. - -// // 2. If upgrade is free to paid, or paid to free (migration / update) -// let toFreeProduct = isFreeProduct(attachParams.prices); -// let paidToFreeProduct = -// isFreeProduct(curCusProduct.customer_prices.map((cp) => cp.price)) && -// !isFreeProduct(attachParams.prices); - -// if (trialToTrial || trialToPaid || toFreeProduct || paidToFreeProduct) { -// if (trialToTrial) { -// logger.info( -// `Upgrading from trial to trial, cancelling and starting new subscription`, -// ); -// } else if (toFreeProduct) { -// logger.info( -// `switching to free product, cancelling (if needed) and adding free product`, -// ); -// } - -// await handleAddProduct({ -// req, -// res, -// attachParams, -// fromRequest: fromReq, -// carryExistingUsages, -// keepResetIntervals: newVersion, // keep reset intervals if upgrading version (migrations) -// disableMerge: true, -// }); - -// if (notNullish(curCusProduct.subscription_ids)) { -// for (const subId of curCusProduct.subscription_ids!) { -// try { -// await stripeCli.subscriptions.cancel(subId); -// } catch (error) { -// throw new RecaseError({ -// message: `Handling upgrade (cur product on trial): failed to cancel subscription ${subId}`, -// code: ErrCode.StripeCancelSubscriptionFailed, -// statusCode: StatusCodes.BAD_REQUEST, -// data: error, -// }); -// } -// } -// } -// return; -// } - -// logger.info("1. Updating current subscription to new product"); -// let { -// subUpdate, -// newSubIds, -// invoiceIds, -// remainingExistingSubIds, -// newSubs, -// }: any = await handleStripeSubUpdate({ -// db: req.db, -// curCusProduct, -// stripeCli, -// attachParams, -// disableFreeTrial, -// stripeSubs, -// logger, -// carryExistingUsages, -// prorationBehavior, -// }); - -// // logger.info("2. Bill for remaining usages"); -// // await billForRemainingUsages({ -// // db: req.db, -// // attachParams, -// // curCusProduct, -// // newSubs, -// // logger, -// // }); - -// logger.info( -// "2.1. Remove old subscription ID from old cus product and expire", -// ); -// await CusProductService.update({ -// db: req.db, -// cusProductId: curCusProduct.id, -// updates: { -// subscription_ids: curCusProduct.subscription_ids!.filter( -// (subId) => subId !== subUpdate.id, -// ), -// processor: { -// ...curCusProduct.processor, -// subscription_id: null, -// } as any, -// status: CusProductStatus.Expired, -// }, -// }); - -// if (remainingExistingSubIds && remainingExistingSubIds.length > 0) { -// logger.info("2.2. Canceling old subscriptions"); -// for (const subId of remainingExistingSubIds) { -// logger.info(" - Cancelling old subscription", subId); -// await stripeCli.subscriptions.cancel(subId); -// } -// } - -// // Handle backend -// logger.info("3. Creating new full cus product"); - -// await createFullCusProduct({ -// db: req.db, -// attachParams: attachToInsertParams(attachParams, products[0]), -// subscriptionIds: newSubIds, - -// anchorToUnix: -// newSubs.length > 0 ? newSubs[0].current_period_end * 1000 : undefined, - -// disableFreeTrial, -// carryExistingUsages, -// carryOverTrial: true, -// scenario: AttachScenario.Upgrade, -// logger, -// }); - -// // Create invoices -// logger.info("4. Creating invoices"); -// logger.info(`Invoice IDs: ${invoiceIds}`); -// const batchInsertInvoice = []; - -// for (const invoiceId of invoiceIds) { -// const insertInvoice = async () => { -// const stripeInvoice = await getStripeExpandedInvoice({ -// stripeCli, -// stripeInvoiceId: invoiceId, -// }); - -// let autumnInvoiceItems = await getInvoiceItems({ -// stripeInvoice, -// prices: attachParams.prices, -// logger, -// }); - -// await InvoiceService.createInvoiceFromStripe({ -// db: req.db, -// stripeInvoice, -// internalCustomerId: customer.internal_id, -// internalEntityId: attachParams.internalEntityId, -// org, -// productIds: products.map((p) => p.id), -// internalProductIds: products.map((p) => p.internal_id), -// items: autumnInvoiceItems, -// }); -// }; -// batchInsertInvoice.push(insertInvoice()); -// } - -// await Promise.all(batchInsertInvoice); -// logger.info("✅ Done!"); - -// if (fromReq) { -// if (org.api_version! >= APIVersion.v1_1) { -// res.status(200).json( -// AttachResultSchema.parse({ -// customer_id: customer.id, -// product_ids: products.map((p) => p.id), -// code: updateSameProduct -// ? SuccessCode.UpdatedSameProduct -// : newVersion -// ? SuccessCode.UpgradedToNewVersion -// : SuccessCode.UpgradedToNewProduct, -// message: `Successfully attached ${product.name} to ${customer.name} -- upgraded from ${curFullProduct.name}`, -// }), -// ); -// } else { -// res.status(200).json({ -// success: true, -// message: `Successfully attached ${product.name} to ${customer.name} -- upgraded from ${curFullProduct.name}`, -// }); -// } -// } -// }; diff --git a/server/src/internal/customers/change-product/scheduleUtils.ts b/server/src/internal/customers/change-product/scheduleUtils.ts index 148015c0a..b4126c78d 100644 --- a/server/src/internal/customers/change-product/scheduleUtils.ts +++ b/server/src/internal/customers/change-product/scheduleUtils.ts @@ -6,6 +6,7 @@ import { AppEnv, AttachScenario, FullCusProduct, + intervalsSame, Organization, Product, } from "@autumn/shared"; @@ -30,6 +31,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; import { notNullish } from "@/utils/genUtils.js"; +import { subToAutumnInterval } from "@/external/stripe/utils.js"; export const getPricesForCusProduct = ({ cusProduct, @@ -134,7 +136,7 @@ export const cancelFutureProductSchedule = async ({ // Case where cur scheduled product is not free for (const scheduleObj of schedules) { - const { schedule, interval, prices } = scheduleObj; + const { schedule, interval, intervalCount, prices } = scheduleObj; if (inIntervals && !inIntervals.includes(interval!)) { continue; @@ -142,7 +144,7 @@ export const cancelFutureProductSchedule = async ({ // 1. Remove cur scheduled product items from schedule const activeCusProducts = cusProducts.filter((cusProduct) => - isActiveStatus(cusProduct?.status), + isActiveStatus(cusProduct?.status) ); const filteredScheduleItems = getFilteredScheduleItems({ @@ -152,8 +154,11 @@ export const cancelFutureProductSchedule = async ({ // 2. If any items left, update schedule with cur main product! if (filteredScheduleItems.length > 0) { - let oldItemSet = oldItemSets.find( - (itemSet) => itemSet.interval === interval, + let oldItemSet = oldItemSets.find((itemSet) => + intervalsSame({ + intervalA: { interval, intervalCount }, + intervalB: itemSet, + }) ); await updateScheduledSubWithNewItems({ @@ -186,7 +191,7 @@ export const cancelFutureProductSchedule = async ({ cusProductId: curMainProduct!.id, updates: { scheduled_ids: curMainProduct!.scheduled_ids?.filter( - (id) => id !== schedule.id, + (id) => id !== schedule.id ), }, }); @@ -202,15 +207,21 @@ export const cancelFutureProductSchedule = async ({ await stripeCli.subscriptionSchedules.cancel(schedule.id); } catch (error: any) { logger.warn( - `❌ Error cancelling schedule: ${schedule.id}, ${error.message}`, + `❌ Error cancelling schedule: ${schedule.id}, ${error.message}` ); } - const subWithSameInterval = curSubs.find( - (sub) => + const subWithSameInterval = curSubs.find((sub) => { + let subInterval = subToAutumnInterval(sub); + // sub.items.data[0]?.price?.recurring?.interval === interval + return ( sub.items.data.length > 0 && - sub.items.data[0]?.price?.recurring?.interval === interval, - ); + intervalsSame({ + intervalA: { interval, intervalCount }, + intervalB: subInterval, + }) + ); + }); if (subWithSameInterval && renewCurProduct) { await stripeCli.subscriptions.update(subWithSameInterval.id, { @@ -291,7 +302,7 @@ export const cancelFutureProductSchedule = async ({ } } catch (error) { logger.error( - `❌ Error sending products updated webhook from cancelFutureProductSchedule: ${error}`, + `❌ Error sending products updated webhook from cancelFutureProductSchedule: ${error}` ); } } @@ -310,7 +321,7 @@ export const cancelFutureProductSchedule = async ({ batchRenew.push( stripeCli.subscriptions.update(subId, { cancel_at: null, - }), + }) ); } diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts index c16f81407..4d327e8ca 100644 --- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts +++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts @@ -204,7 +204,7 @@ export const getCusBalances = async ({ const cusProduct = cusEnt.customer_product; const feature = cusEnt.entitlement.feature; const ent: EntitlementWithFeature = cusEnt.entitlement; - let key = `${ent.interval || "no-interval"}-${feature.id}`; + let key = `${ent.interval || "no-interval"}-${ent.interval_count || 1}-${feature.id}`; // 1. Handle boolean let isBoolean = feature.type == FeatureType.Boolean; @@ -239,6 +239,11 @@ export const getCusBalances = async ({ unlimited: isBoolean ? undefined : unlimited, interval: isBoolean || unlimited ? undefined : ent.interval || undefined, + // interval_count: + // isBoolean || unlimited + // ? undefined + // : ent.interval_count || undefined, + balance: isBoolean ? undefined : unlimited ? null : 0, total: isBoolean || unlimited ? undefined : 0, adjustment: isBoolean || unlimited ? undefined : 0, diff --git a/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts b/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts index de8bee69f..dd1112f0a 100644 --- a/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts +++ b/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts @@ -51,6 +51,7 @@ import { import { sortPricesByType } from "@/internal/products/prices/priceUtils/sortPriceUtils.js"; import { getMergeCusProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.js"; import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; +import { intervalsDifferent } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; export const getDefaultPriceStr = ({ org, @@ -83,6 +84,7 @@ export const getProration = ({ anchorToUnix, now, interval, + intervalCount, }: { proration?: { start: number; @@ -90,6 +92,7 @@ export const getProration = ({ }; anchorToUnix?: number; interval: BillingInterval; + intervalCount: number; now: number; }) => { if (!proration && !anchorToUnix) return undefined; @@ -103,11 +106,16 @@ export const getProration = ({ let end = getAlignedIntervalUnix({ alignWithUnix: anchorToUnix!, interval, + intervalCount, now, alwaysReturn: true, }); - let start = subtractBillingIntervalUnix(end!, interval); + let start = subtractBillingIntervalUnix({ + unixTimestamp: end!, + interval, + intervalCount, + }); return { start, @@ -121,6 +129,7 @@ export const getItemsForNewProduct = async ({ now, proration, interval, + intervalCount, anchorToUnix, freeTrial, stripeSubs, @@ -137,6 +146,7 @@ export const getItemsForNewProduct = async ({ end: number; }; interval?: BillingInterval; + intervalCount?: number; anchorToUnix?: number; freeTrial?: FreeTrial | null; stripeSubs?: Stripe.Subscription[]; @@ -156,13 +166,27 @@ export const getItemsForNewProduct = async ({ const ent = getPriceEntitlement(price, newProduct.entitlements); const billingType = getBillingType(price.config); - if (interval && price.config.interval !== interval) continue; + if ( + interval && + intervalsDifferent({ + intervalA: { + interval: interval, + intervalCount: intervalCount, + }, + intervalB: { + interval: price.config.interval, + intervalCount: price.config.interval_count, + }, + }) + ) + continue; const finalProration = getProration({ proration, anchorToUnix, now, interval: price.config.interval!, + intervalCount: price.config.interval_count!, }); if (isFixedPrice({ price })) { diff --git a/server/src/internal/products/handlers/handleListProductsBeta.ts b/server/src/internal/products/handlers/handleListProductsBeta.ts index a8f57dd37..00e9f4af0 100644 --- a/server/src/internal/products/handlers/handleListProductsBeta.ts +++ b/server/src/internal/products/handlers/handleListProductsBeta.ts @@ -1,9 +1,7 @@ import { routeHandler } from "@/utils/routerUtils.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; -import { CusService } from "@/internal/customers/CusService.js"; import { sortFullProducts } from "../productUtils/sortProductUtils.js"; -import { toPricecnProduct } from "../pricecn/pricecnUtils.js"; import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js"; import { getCusWithCache } from "@/internal/customers/cusCache/getCusWithCache.js"; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts index 26944a756..e24c91156 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts @@ -42,13 +42,13 @@ const productDetailsSame = (prod1: Product, prod2: UpdateProduct) => { if (notNullish(prod2.is_default) && prod1.is_default != prod2.is_default) { return false; } - console.log( - "prod1.archived", - prod1.archived, - prod2.archived, - notNullish(prod2.archived), - prod1.archived !== prod2.archived - ); + // console.log( + // "prod1.archived", + // prod1.archived, + // prod2.archived, + // notNullish(prod2.archived), + // prod1.archived !== prod2.archived + // ); if (notNullish(prod2.archived) && prod1.archived !== prod2.archived) { return false; } diff --git a/server/src/internal/products/pricecn/pricecnUtils.ts b/server/src/internal/products/pricecn/pricecnUtils.ts index e561bd32a..371db3623 100644 --- a/server/src/internal/products/pricecn/pricecnUtils.ts +++ b/server/src/internal/products/pricecn/pricecnUtils.ts @@ -22,7 +22,7 @@ import { isPriceItem } from "../product-items/productItemUtils/getItemType.js"; import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js"; import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { isProductUpgrade } from "../productUtils.js"; -import { getFirstInterval } from "../prices/priceUtils/priceIntervalUtils.js"; +import { getLargestInterval } from "../prices/priceUtils/priceIntervalUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { getFreeTrialAfterFingerprint } from "../free-trials/freeTrialUtils.js"; @@ -388,7 +388,7 @@ export const toPricecnProduct = async ({ let baseVariant = null; if (fullProduct.base_variant_id) { baseVariant = otherProducts.find( - (p) => p.id == fullProduct.base_variant_id, + (p) => p.id == fullProduct.base_variant_id ); } @@ -402,7 +402,8 @@ export const toPricecnProduct = async ({ baseVariant || otherProducts.some((p) => p.base_variant_id == product.id) ) { - intervalGroup = getFirstInterval({ prices: fullProduct.prices }); + let intervalSet = getLargestInterval({ prices: fullProduct.prices }); + intervalGroup = intervalSet?.interval; } let trialAvailable = false; diff --git a/server/src/internal/products/prices/billingIntervalUtils.ts b/server/src/internal/products/prices/billingIntervalUtils.ts index a46685e8c..030de798b 100644 --- a/server/src/internal/products/prices/billingIntervalUtils.ts +++ b/server/src/internal/products/prices/billingIntervalUtils.ts @@ -21,24 +21,29 @@ import { import { UTCDate } from "@date-fns/utc"; import { formatUnixToDate, formatUnixToDateTime } from "@/utils/genUtils.js"; -export const subtractBillingIntervalUnix = ( - unixTimestamp: number, - interval: BillingInterval, -) => { +export const subtractBillingIntervalUnix = ({ + unixTimestamp, + interval, + intervalCount, +}: { + unixTimestamp: number; + interval: BillingInterval; + intervalCount: number; +}) => { const date = new UTCDate(unixTimestamp); let subtractedDate = date; switch (interval) { case BillingInterval.Month: - subtractedDate = subMonths(date, 1); + subtractedDate = subMonths(date, 1 * intervalCount); break; case BillingInterval.Quarter: - subtractedDate = subMonths(date, 3); + subtractedDate = subMonths(date, 3 * intervalCount); break; case BillingInterval.SemiAnnual: - subtractedDate = subMonths(date, 6); + subtractedDate = subMonths(date, 6 * intervalCount); break; case BillingInterval.Year: - subtractedDate = subYears(date, 1); + subtractedDate = subYears(date, 1 * intervalCount); break; default: throw new Error(`Invalid billing interval: ${interval}`); @@ -46,24 +51,29 @@ export const subtractBillingIntervalUnix = ( return subtractedDate.getTime(); }; -export const addBillingIntervalUnix = ( - unixTimestamp: number, - interval: BillingInterval, -) => { +export const addBillingIntervalUnix = ({ + unixTimestamp, + interval, + intervalCount, +}: { + unixTimestamp: number; + interval: BillingInterval; + intervalCount: number; +}) => { const date = new UTCDate(unixTimestamp); let addedDate = date; switch (interval) { case BillingInterval.Month: - addedDate = addMonths(date, 1); + addedDate = addMonths(date, intervalCount); break; case BillingInterval.Quarter: - addedDate = addMonths(date, 3); + addedDate = addMonths(date, 3 * intervalCount); break; case BillingInterval.SemiAnnual: - addedDate = addMonths(date, 6); + addedDate = addMonths(date, 6 * intervalCount); break; case BillingInterval.Year: - addedDate = addYears(date, 1); + addedDate = addYears(date, 1 * intervalCount); break; default: throw new Error(`Invalid billing interval: ${interval}`); @@ -71,8 +81,18 @@ export const addBillingIntervalUnix = ( return addedDate.getTime(); }; -export const getNextStartOfMonthUnix = (interval: BillingInterval) => { - const nextBillingCycle = addBillingIntervalUnix(Date.now(), interval); +export const getNextStartOfMonthUnix = ({ + interval, + intervalCount, +}: { + interval: BillingInterval; + intervalCount: number; +}) => { + const nextBillingCycle = addBillingIntervalUnix({ + unixTimestamp: Date.now(), + interval, + intervalCount, + }); // Subtract till it hits first const date = new UTCDate(nextBillingCycle); @@ -85,11 +105,13 @@ export const getNextStartOfMonthUnix = (interval: BillingInterval) => { export const getAlignedIntervalUnix = ({ alignWithUnix, interval, + intervalCount, now, alwaysReturn, }: { alignWithUnix: number; interval: BillingInterval; + intervalCount: number; now?: number; alwaysReturn?: boolean; }) => { @@ -99,7 +121,11 @@ export const getAlignedIntervalUnix = ({ now = now || Date.now(); - const naturalBillingDate = addBillingIntervalUnix(now, interval); + const naturalBillingDate = addBillingIntervalUnix({ + unixTimestamp: now, + interval, + intervalCount, + }); // console.log("Now:", formatUnixToDateTime(now)); // console.log("Anchoring to:", formatUnixToDateTime(alignWithUnix)); @@ -108,10 +134,11 @@ export const getAlignedIntervalUnix = ({ const maxIterations = 10000; let iterations = 0; while (true) { - const subtractedUnix = subtractBillingIntervalUnix( - nextCycleAnchorUnix, + const subtractedUnix = subtractBillingIntervalUnix({ + unixTimestamp: nextCycleAnchorUnix, interval, - ); + intervalCount, + }); // console.log("Subtracted unix:", formatUnixToDateTime(subtractedUnix)); @@ -134,12 +161,12 @@ export const getAlignedIntervalUnix = ({ let anchorAndNaturalDiff = differenceInSeconds( naturalBillingDate, - nextCycleAnchorUnix, + nextCycleAnchorUnix ); // For insurance, also means you can't set billing cycle anchor to a minute in the future... let anchorAndNowDiff = Math.abs( - differenceInSeconds(now, nextCycleAnchorUnix), + differenceInSeconds(now, nextCycleAnchorUnix) ); if (anchorAndNaturalDiff < 60 || anchorAndNowDiff < 20) { @@ -182,7 +209,7 @@ export const subtractFromUnixTillAligned = ({ const lastDayOfMonth = new UTCDate( alignedDate.getFullYear(), alignedDate.getMonth() + 1, - 0, + 0 ).getDate(); // Apply target day (capped to last day of month) and time components diff --git a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts index 9e653be3c..cd29ac334 100644 --- a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts +++ b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts @@ -15,6 +15,7 @@ export const subItemToFixedPrice = ({ }) => { const { price } = subItem; + const { interval, intervalCount } = subItemToAutumnInterval(subItem); return constructPrice({ internalProductId: product.internal_id, isCustom: true, @@ -22,7 +23,8 @@ export const subItemToFixedPrice = ({ fixedConfig: { type: PriceType.Fixed, amount: basePrice || (price.unit_amount || 0) / 100, - interval: subItemToAutumnInterval(subItem)!, + interval, + interval_count: intervalCount, stripe_price_id: price.id, }, }); diff --git a/server/src/internal/products/prices/priceUtils/convertPrice.ts b/server/src/internal/products/prices/priceUtils/convertPrice.ts index 7241e9fff..617c49a0f 100644 --- a/server/src/internal/products/prices/priceUtils/convertPrice.ts +++ b/server/src/internal/products/prices/priceUtils/convertPrice.ts @@ -12,7 +12,31 @@ import { getBillingType, getPriceEntitlement } from "../priceUtils.js"; import { isFixedPrice } from "./usagePriceUtils/classifyUsagePrice.js"; export const priceToIntervalKey = (price: Price) => { - return `${price.config.interval}-${price.config.interval_count ?? 1}`; + return toIntervalKey({ + interval: price.config?.interval, + intervalCount: price.config?.interval_count ?? 1, + }); +}; + +export const toIntervalKey = ({ + interval, + intervalCount, +}: { + interval: BillingInterval; + intervalCount: number; +}) => { + if (interval == BillingInterval.OneOff) { + return BillingInterval.OneOff; + } else if (interval == BillingInterval.Quarter) { + let finalCount = (intervalCount ?? 1) * 3; + return `${BillingInterval.Month}-${finalCount}`; + } else if (interval == BillingInterval.SemiAnnual) { + let finalCount = (intervalCount ?? 1) * 6; + return `${BillingInterval.Month}-${finalCount}`; + } else if (interval == BillingInterval.Year) { + return BillingInterval.Year; + } + return `${interval}-${intervalCount}`; }; export const intervalKeyToPrice = (intervalKey: string) => { diff --git a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts index 02758ef38..bb74b82b8 100644 --- a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts +++ b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts @@ -2,7 +2,9 @@ import { BillingInterval, EntInterval, Entitlement, + FixedPriceConfig, Price, + UsagePriceConfig, } from "@autumn/shared"; import { nullish } from "@/utils/genUtils.js"; @@ -28,60 +30,169 @@ const entToBillingInterval = (entInterval: EntInterval | null | undefined) => { } else return entInterval as unknown as BillingInterval; }; -export function compareBillingIntervals( - a: BillingInterval | undefined, - b: BillingInterval | undefined, -): number { - if (nullish(a)) { - return 1; - } else if (nullish(b)) { - return -1; - } +const intervalToValue = ( + interval: BillingInterval, + intervalCount?: number | null +) => { + const intervalToBaseVal: Record = { + [BillingInterval.OneOff]: 0, + [BillingInterval.Month]: 1, + [BillingInterval.Quarter]: 3, + [BillingInterval.SemiAnnual]: 6, + [BillingInterval.Year]: 12, + }; - return BillingIntervalOrder.indexOf(a!) - BillingIntervalOrder.indexOf(b!); + return intervalToBaseVal[interval] * (intervalCount ?? 1); +}; + +type IntervalConfig = { + interval: BillingInterval; + intervalCount?: number | null; +}; + +export function compareBillingIntervals({ + configA, + configB, +}: { + configA: IntervalConfig; + configB: IntervalConfig; +}): number { + // if (nullish(a)) { + // return 1; + // } else if (nullish(b)) { + // return -1; + // } + + // how to compare these... convert to months? + + const a = intervalToValue(configA.interval, configA.intervalCount); + const b = intervalToValue(configB.interval, configB.intervalCount); + + return b - a; } -export const getFirstInterval = ({ +export const getLargestInterval = ({ prices, excludeOneOff = false, }: { prices: Price[]; excludeOneOff?: boolean; }) => { - return BillingIntervalOrder.find((interval) => - prices.some((price) => { - let intervalMatch = price.config.interval === interval; - let oneOffMatch = excludeOneOff - ? price.config.interval !== BillingInterval.OneOff - : true; - return intervalMatch && oneOffMatch; - }), - )!; + let sortedPrices = structuredClone(prices); + sortPricesByInterval(sortedPrices); + + if (excludeOneOff) { + sortedPrices = sortedPrices.filter( + (price) => price.config.interval !== BillingInterval.OneOff + ); + } + + if (sortedPrices.length === 0) { + return null; + } + + return { + interval: sortedPrices[0].config.interval, + intervalCount: sortedPrices[0].config.interval_count ?? 1, + }; + + // return BillingIntervalOrder.find((interval) => + // prices.some((price) => { + // let intervalMatch = price.config.interval === interval; + // let oneOffMatch = excludeOneOff + // ? price.config.interval !== BillingInterval.OneOff + // : true; + // return intervalMatch && oneOffMatch; + // }) + // )!; }; -export const getLastInterval = ({ +export const getSmallestInterval = ({ prices, ents, }: { prices: Price[]; ents?: Entitlement[]; }) => { - return ReversedBillingIntervalOrder.find( - (interval) => - prices.some((price) => price.config.interval === interval) || - (ents && - ents?.some((ent) => entToBillingInterval(ent.interval) === interval)), - )!; -}; - -export const sortBillingIntervals = (intervals: BillingInterval[]) => { - return intervals.sort((a, b) => { - return BillingIntervalOrder.indexOf(a) - BillingIntervalOrder.indexOf(b); + // let sortedPrices = structuredClone(prices); + // sortPricesByInterval(sortedPrices); + const allPriceIntervals = prices.map((p) => { + return { + interval: p.config.interval, + intervalCount: p.config.interval_count ?? 1, + }; }); + + const allEntIntervals = ents?.map((e) => { + return { + interval: entToBillingInterval(e.interval), + intervalCount: e.interval_count ?? 1, + }; + }); + + const allIntervals = [...allPriceIntervals, ...(allEntIntervals || [])]; + + if (allIntervals.length === 0) { + return null; + } + + allIntervals.sort((a, b) => { + return compareBillingIntervals({ configA: a, configB: b }); + }); + + const smallestInterval = allIntervals?.[0]; + + return { + interval: smallestInterval.interval, + intervalCount: smallestInterval.intervalCount, + }; + + // if (!smallestIntervalPrice) { + // return null; + // } + + // return { + // interval: smallestIntervalPrice.config.interval, + // intervalCount: smallestIntervalPrice.config.interval_count ?? 1, + // }; + + // return ReversedBillingIntervalOrder.find( + // (interval) => + // prices.some((price) => price.config.interval === interval) || + // (ents && + // ents?.some((ent) => entToBillingInterval(ent.interval) === interval)) + // )!; }; export const sortPricesByInterval = (prices: Price[]) => { return prices.sort((a, b) => { - return compareBillingIntervals(a.config.interval, b.config.interval); + return compareBillingIntervals({ configA: a.config, configB: b.config }); }); }; + +export const intervalsDifferent = ({ + intervalA, + intervalB, +}: { + intervalA: IntervalConfig | null; + intervalB: IntervalConfig | null; +}) => { + // return compareBillingIntervals({ configA: intervalA, configB: intervalB }) !== 0; + if (nullish(intervalA) && nullish(intervalB)) { + return false; + } + + if (nullish(intervalA) || nullish(intervalB)) { + return true; + } + + const intervalCountA = intervalToValue( + intervalA!.interval, + intervalA!.intervalCount + ); + const intervalCountB = intervalToValue( + intervalB!.interval, + intervalB!.intervalCount + ); + return intervalCountA !== intervalCountB; +}; diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts index 0dc741e7c..3a4f8e212 100644 --- a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts +++ b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts @@ -10,6 +10,7 @@ import { Organization, ProductItem, ProductItemFeatureType, + ProductItemInterval, } from "@autumn/shared"; import { isFeatureItem, @@ -17,6 +18,20 @@ import { isPriceItem, } from "../../product-items/productItemUtils/getItemType.js"; +const getIntervalString = ({ + interval, + intervalCount, +}: { + interval: ProductItemInterval; + intervalCount?: number | null; +}) => { + if (!interval) return ""; + if (intervalCount == 1) { + return `per ${interval}`; + } + return `per ${intervalCount} ${interval}s`; +}; + export const formatTiers = ({ item, currency, @@ -112,7 +127,12 @@ export const getPriceItemDisplay = ({ currency, amount: item.price as number, }); - let secondaryText = item.interval ? `per ${item.interval}` : undefined; + let intervalStr = getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }); + + let secondaryText = intervalStr || undefined; return { primary_text: primaryText, @@ -170,7 +190,16 @@ export const getFeaturePriceItemDisplay = ({ priceStr2 = `${billingFeatureName}`; } - let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : ""; + // let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : ""; + let intervalStr = isMainPrice + ? getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }) + : ""; + + // console.log("isMainPrice", isMainPrice); + // console.log("intervalStr", intervalStr); if (includedUsageStr) { return { @@ -179,8 +208,16 @@ export const getFeaturePriceItemDisplay = ({ }; } + if (isMainPrice) { + return { + primary_text: priceStr + ` per ${priceStr2}`, + secondary_text: `${intervalStr}`, + }; + } + + // ${intervalStr} return { - primary_text: priceStr + ` per ${priceStr2}${intervalStr}`, + primary_text: priceStr + ` per ${priceStr2}`, // secondary_text: `per ${priceStr2}${intervalStr}`, secondary_text: "", }; @@ -190,10 +227,12 @@ export const getProductItemDisplay = ({ item, features, currency = "usd", + isMainPrice = false, }: { item: ProductItem; features: Feature[]; currency?: string | null; + isMainPrice?: boolean; }) => { if (isFeatureItem(item)) { return getFeatureItemDisplay({ @@ -214,6 +253,7 @@ export const getProductItemDisplay = ({ item, feature: features.find((f) => f.id === item.feature_id), currency, + isMainPrice, }); } diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts index 9f1df6f68..99500f23c 100644 --- a/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts +++ b/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts @@ -24,7 +24,7 @@ import { getFreeTrialAfterFingerprint } from "../../free-trials/freeTrialUtils.j import { DrizzleCli } from "@/db/initDrizzle.js"; import { notNullish } from "@/utils/genUtils.js"; import { isFreeProduct, isOneOff } from "../../productUtils.js"; -import { getFirstInterval } from "../../prices/priceUtils/priceIntervalUtils.js"; +import { getLargestInterval } from "../../prices/priceUtils/priceIntervalUtils.js"; import { itemToPriceOrTiers } from "../../product-items/productItemUtils.js"; import { toAPIFeature } from "@/internal/features/utils/mapFeatureUtils.js"; import { isPrepaidPrice } from "../../prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; @@ -35,12 +35,14 @@ export const getProductItemResponse = ({ currency, withDisplay = true, options, + isMainPrice = false, }: { item: ProductItem; features: Feature[]; currency?: string | null; withDisplay?: boolean; options?: FeatureOptions[]; + isMainPrice?: boolean; }) => { // 1. Get item type let type = getItemType(item); @@ -50,6 +52,7 @@ export const getProductItemResponse = ({ item, features, currency, + isMainPrice, }); let priceData = itemToPriceOrTiers({ item }); @@ -130,10 +133,7 @@ export const getProductProperties = ({ product: FullProduct; freeTrial?: FreeTrialResponse | null; }) => { - let firstInterval: any = getFirstInterval({ prices: product.prices }); - if (firstInterval == BillingInterval.OneOff) { - firstInterval = null; - } + const largestInterval = getLargestInterval({ prices: product.prices }); let hasFreeTrial = notNullish(freeTrial) && freeTrial?.trial_available !== false; @@ -141,7 +141,7 @@ export const getProductProperties = ({ return ProductPropertiesSchema.parse({ is_free: isFreeProduct(product.prices) || false, is_one_off: isOneOff(product.prices) || false, - interval_group: firstInterval, + interval_group: largestInterval?.interval, has_trial: hasFreeTrial, updateable: product.prices.some( (p: Price) => @@ -179,13 +179,14 @@ export const getProductResponse = async ({ let sortedItems = sortProductItems(rawItems, features); // Transform sorted items - let items = sortedItems.map((item) => { + let items = sortedItems.map((item, index) => { return getProductItemResponse({ item, features, currency, withDisplay, options, + isMainPrice: index == 0, }); }); diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index 27a4f9c1d..825106cad 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -447,8 +447,12 @@ export const stripeToAutumnInterval = ({ }; export const subItemToAutumnInterval = (item: Stripe.SubscriptionItem) => { - return stripeToAutumnInterval({ - interval: item.price.recurring?.interval!, - intervalCount: item.price.recurring?.interval_count!, - }); + return { + interval: item.price.recurring?.interval as BillingInterval, + intervalCount: item.price.recurring?.interval_count || 1, + }; + // return stripeToAutumnInterval({ + // interval: item.price.recurring?.interval!, + // intervalCount: item.price.recurring?.interval_count!, + // }); }; diff --git a/server/tests/utils/testProductUtils/testProductUtils.ts b/server/tests/utils/testProductUtils/testProductUtils.ts index 3b7fb33a8..1ac865a53 100644 --- a/server/tests/utils/testProductUtils/testProductUtils.ts +++ b/server/tests/utils/testProductUtils/testProductUtils.ts @@ -29,11 +29,13 @@ export const addPrefixToProducts = ({ export const replaceItems = ({ featureId, interval, + intervalCount, newItem, items, }: { featureId?: string; interval?: BillingInterval; + intervalCount?: number; newItem: ProductItem; items: ProductItem[]; }) => { @@ -46,7 +48,10 @@ export const replaceItems = ({ if (interval) { index = newItems.findIndex( - (item) => item.interval == (interval as any) && nullish(item.feature_id), + (item) => + item.interval == (interval as any) && + (intervalCount ? item.interval_count == intervalCount : true) && + nullish(item.feature_id) ); } diff --git a/shared/index.ts b/shared/index.ts index aa2cd2a81..69b259c29 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -143,6 +143,7 @@ export * from "./models/chatResultModels/chatResultFeature.js"; export * from "./utils/productDisplayUtils/getProductItemRes.js"; export * from "./utils/productUtils.js"; export * from "./utils/productDisplayUtils/sortProductItems.js"; +export * from "./utils/intervalUtils.js"; // ENUMS export * from "./enums/SuccessCode.js"; diff --git a/shared/models/cusModels/cusResModels/cusFeatureResponse.ts b/shared/models/cusModels/cusResModels/cusFeatureResponse.ts index 1caae1a8c..d571e540e 100644 --- a/shared/models/cusModels/cusResModels/cusFeatureResponse.ts +++ b/shared/models/cusModels/cusResModels/cusFeatureResponse.ts @@ -10,6 +10,7 @@ export const CusRolloverSchema = z.object({ export const CusEntResponseSchema = z.object({ feature_id: z.string(), interval: z.nativeEnum(EntInterval).nullish(), + interval_count: z.number().nullish(), unlimited: z.boolean().nullish(), balance: z.number().nullish(), // usage: z.number().nullish(), @@ -22,6 +23,7 @@ export const CusEntResponseSchema = z.object({ export const CoreCusFeatureResponseSchema = z.object({ interval: z.nativeEnum(EntInterval).or(z.literal("multiple")).nullish(), + interval_count: z.number().nullish(), unlimited: z.boolean().nullish(), balance: z.number().nullish(), usage: z.number().nullish(), @@ -33,6 +35,7 @@ export const CoreCusFeatureResponseSchema = z.object({ .array( z.object({ interval: z.nativeEnum(EntInterval), + interval_count: z.number().nullish(), balance: z.number().nullish(), usage: z.number().nullish(), included_usage: z.number().nullish(), diff --git a/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts b/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts index fea374680..59919e511 100644 --- a/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts +++ b/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts @@ -20,6 +20,7 @@ export const ProductItemResponseSchema = z.object({ included_usage: z.number().or(z.literal(Infinite)).nullish(), interval: z.nativeEnum(ProductItemInterval).nullish(), + interval_count: z.number().nullish(), // Price config price: z.number().nullish(), diff --git a/shared/utils/intervalUtils.ts b/shared/utils/intervalUtils.ts new file mode 100644 index 000000000..15fb535e2 --- /dev/null +++ b/shared/utils/intervalUtils.ts @@ -0,0 +1,43 @@ +import { BillingInterval } from "../models/productModels/priceModels/priceEnums.js"; + +export const intervalToValue = ( + interval: BillingInterval, + intervalCount?: number | null +) => { + const intervalToBaseVal: Record = { + [BillingInterval.OneOff]: 0, + [BillingInterval.Month]: 1, + [BillingInterval.Quarter]: 3, + [BillingInterval.SemiAnnual]: 6, + [BillingInterval.Year]: 12, + }; + + return intervalToBaseVal[interval] * (intervalCount ?? 1); +}; + +export type IntervalConfig = { + interval: BillingInterval; + intervalCount?: number | null; +}; + +export const intervalsDifferent = ({ + intervalA, + intervalB, +}: { + intervalA: IntervalConfig; + intervalB: IntervalConfig; +}) => { + let valA = intervalToValue(intervalA.interval, intervalA.intervalCount); + let valB = intervalToValue(intervalB.interval, intervalB.intervalCount); + return valA != valB; +}; + +export const intervalsSame = ({ + intervalA, + intervalB, +}: { + intervalA: IntervalConfig; + intervalB: IntervalConfig; +}) => { + return !intervalsDifferent({ intervalA, intervalB }); +}; diff --git a/shared/utils/productDisplayUtils.ts b/shared/utils/productDisplayUtils.ts index 39b257f2f..692cbda58 100644 --- a/shared/utils/productDisplayUtils.ts +++ b/shared/utils/productDisplayUtils.ts @@ -3,6 +3,7 @@ import { Infinite } from "../models/productModels/productEnums.js"; import { ProductItem, ProductItemFeatureType, + ProductItemInterval, } from "../models/productV2Models/productItemModels/productItemModels.js"; import { getFeatureName, @@ -45,6 +46,20 @@ export const formatTiers = ({ } }; +export const getIntervalString = ({ + interval, + intervalCount, +}: { + interval: ProductItemInterval; + intervalCount?: number | null; +}) => { + if (!interval) return ""; + if (intervalCount == 1) { + return `per ${interval}`; + } + return `per ${intervalCount} ${interval}s`; +}; + export const getFeatureItemDisplay = ({ item, feature, @@ -148,18 +163,30 @@ export const getFeaturePriceItemDisplay = ({ priceStr2 = `${billingFeatureName}`; } - let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : ""; + // let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : ""; + let intervalStr = isMainPrice + ? getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }) + : ""; if (includedUsageStr) { return { primary_text: includedUsageStr, - secondary_text: `then ${priceStr} per ${priceStr2}${intervalStr}`, + secondary_text: `then ${priceStr} per ${priceStr2} ${intervalStr}`, + }; + } + + if (isMainPrice) { + return { + primary_text: priceStr, + secondary_text: `per ${priceStr2} ${intervalStr}`, }; } return { - primary_text: priceStr + ` per ${priceStr2}${intervalStr}`, - // secondary_text: `per ${priceStr2}${intervalStr}`, + primary_text: priceStr + ` per ${priceStr2} ${intervalStr}`, secondary_text: "", }; }; diff --git a/vite/src/utils/product/priceUtils.ts b/vite/src/utils/product/priceUtils.ts index 4d320e223..105d3cb0d 100644 --- a/vite/src/utils/product/priceUtils.ts +++ b/vite/src/utils/product/priceUtils.ts @@ -12,33 +12,9 @@ import { FixedPriceConfig, Price, UsagePriceConfig } from "@autumn/shared"; import { intervalIsNone } from "./productItemUtils"; import { isFeatureItem } from "./getItemType"; -export const validBillingInterval = ( - prices: Price[], - config: FixedPriceConfig | UsagePriceConfig, -) => { - const interval1 = config.interval; - if (!interval1 || interval1 == BillingInterval.OneOff) { - return true; - } - - for (const price of prices) { - const interval2 = price.config?.interval; - - if (!interval2 || interval2 == BillingInterval.OneOff) { - continue; - } - - if (interval1 != interval2) { - return false; - } - } - - return true; -}; - export const getBillingUnits = ( config: UsagePriceConfig, - entitlements: EntitlementWithFeature[], + entitlements: EntitlementWithFeature[] ) => { if (!entitlements) return "(error)"; @@ -51,7 +27,7 @@ export const getBillingUnits = ( } const entitlement = entitlements?.find( - (e) => e.internal_feature_id == config?.internal_feature_id, + (e) => e.internal_feature_id == config?.internal_feature_id ); if (!entitlement) return "n"; @@ -67,6 +43,7 @@ export const getDefaultPriceConfig = (type: PriceType) => { type: PriceType.Fixed, amount: "", interval: BillingInterval.Month, + interval_count: 1, }; } @@ -76,6 +53,7 @@ export const getDefaultPriceConfig = (type: PriceType) => { feature_id: "", bill_when: BillWhen.EndOfPeriod, interval: BillingInterval.Month, + interval_count: 1, billing_units: 1, usage_tiers: [ { @@ -90,7 +68,7 @@ export const getDefaultPriceConfig = (type: PriceType) => { export const isOneOffProduct = ( items: ProductItem[], - isAddOn: boolean = false, + isAddOn: boolean = false ) => { const prices = items.filter((item) => !isFeatureItem(item)); diff --git a/vite/src/utils/product/product-item/formatProductItem.ts b/vite/src/utils/product/product-item/formatProductItem.ts index 72e5cbad0..884422834 100644 --- a/vite/src/utils/product/product-item/formatProductItem.ts +++ b/vite/src/utils/product/product-item/formatProductItem.ts @@ -1,4 +1,5 @@ import { + BillingInterval, Feature, FeatureType, Infinite, @@ -9,6 +10,23 @@ import { import { formatAmount, getItemType, intervalIsNone } from "../productItemUtils"; import { getFeature } from "../entitlementUtils"; import { notNullish } from "@/utils/genUtils"; +import { ProductItemInterval } from "autumn-js"; + +const getIntervalString = ({ + interval, + intervalCount = 1, +}: { + interval: ProductItemInterval; + intervalCount?: number | null; +}) => { + if (!interval) return ""; + + if (intervalCount == 1) { + return `per ${interval}`; + } + + return `per ${intervalCount} ${interval}s`; +}; export const getPaidFeatureString = ({ item, @@ -48,7 +66,11 @@ export const getPaidFeatureString = ({ }`; if (!intervalIsNone(item.interval)) { - amountStr += ` per ${item.interval}`; + const intervalStr = getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }); + amountStr += ` ${intervalStr}`; } if (item.included_usage) { @@ -72,7 +94,11 @@ const getFixedPriceString = ({ }); if (!intervalIsNone(item.interval)) { - return `${formattedAmount} per ${item.interval}`; + const intervalStr = getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }); + return `${formattedAmount} ${intervalStr}`; } return `${formattedAmount}`; @@ -95,7 +121,12 @@ export const getFeatureString = ({ return `Unlimited ${feature?.name}`; } - return `${item.included_usage ?? 0} ${feature?.name}${item.entity_feature_id ? ` per ${getFeature(item.entity_feature_id, features)?.name}` : ""}${notNullish(item.interval) ? ` per ${item.interval}` : ""}`; + const intervalStr = getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }); + + return `${item.included_usage ?? 0} ${feature?.name}${item.entity_feature_id ? ` per ${getFeature(item.entity_feature_id, features)?.name}` : ""}${notNullish(item.interval) ? ` ${intervalStr}` : ""}`; }; export const formatProductItemText = ({ diff --git a/vite/src/views/products/product/prices/CreateFixedPrice.tsx b/vite/src/views/products/product/prices/CreateFixedPrice.tsx index 7d74270d1..563b85f2a 100644 --- a/vite/src/views/products/product/prices/CreateFixedPrice.tsx +++ b/vite/src/views/products/product/prices/CreateFixedPrice.tsx @@ -4,6 +4,7 @@ import { useProductContext } from "../ProductContext"; import { SelectCycle } from "../product-item/product-item-config/components/feature-price/SelectBillingCycle"; import { useProductItemContext } from "../product-item/ProductItemContext"; import { + intervalsDifferent, ProductItem, ProductItemInterval, UpdateProductSchema, @@ -61,7 +62,18 @@ function CreateFixedPrice({ return null; } - if (item.interval == curFixedPrice.interval) { + const intervalsDiff = intervalsDifferent({ + intervalA: { + interval: item.interval, + intervalCount: item.interval_count, + }, + intervalB: { + interval: curFixedPrice.interval, + intervalCount: curFixedPrice.interval_count, + }, + }); + + if (!intervalsDiff) { return null; } @@ -70,6 +82,10 @@ function CreateFixedPrice({ ? "an annual" : `a ${newVariantMap[item.interval! as ProductItemInterval]}`; + if (item.interval_count > 1) { + return `A fixed price already exists on this product. If you're looking to create a version with a different interval, you should create a new product instead.`; + } + return `A fixed price already exists on this product. If you're looking to create ${newIntervalText} version, you should create a new product instead.`; }; diff --git a/vite/src/views/products/product/prices/PricingConfig.tsx b/vite/src/views/products/product/prices/PricingConfig.tsx index 095acc0c7..bb2205bb0 100644 --- a/vite/src/views/products/product/prices/PricingConfig.tsx +++ b/vite/src/views/products/product/prices/PricingConfig.tsx @@ -37,7 +37,7 @@ export const PricingConfig = ({ }; const [priceType, setPriceType] = useState( - priceConfig?.type || PriceType.Fixed, + priceConfig?.type || PriceType.Fixed ); const [name, setName] = useState(priceConfig?.name || ""); @@ -45,19 +45,19 @@ export const PricingConfig = ({ const [fixedConfig, setFixedConfig] = useState( priceConfig && priceConfig.type == PriceType.Fixed ? priceConfig - : defaultFixedConfig, + : defaultFixedConfig ); const [usageConfig, setUsageConfig]: any = useState( priceConfig?.config && priceConfig.config.type == PriceType.Usage ? priceConfig.config - : defaultUsageConfig, + : defaultUsageConfig ); const [originalPrice, _] = useState(priceConfig); useEffect(() => { setPriceConfig( - fixedConfig, + fixedConfig // { // ...originalPrice, // name: name, @@ -163,7 +163,7 @@ export const validateUsageConfig = (usageConfig: any) => { parseFloat(tier.to) < parseFloat(tier.from) ) { toast.error( - "Each tier's 'to' value must be greater than its 'from' value", + "Each tier's 'to' value must be greater than its 'from' value" ); return null; } @@ -179,11 +179,6 @@ export const validateUsageConfig = (usageConfig: any) => { // Validate fixed price config export const validateFixedConfig = (fixedConfig: any) => { - // if (!validBillingInterval(prices, fixedConfig)) { - // toast.error("Can't have two prices with different billing intervals"); - // return null; - // } - const config = { ...fixedConfig }; if (invalidNumber(config.amount) || !config.interval) { From 758e6bcb0d2ed9925286cfa3fc605369b88604a0 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 7 Aug 2025 12:27:04 -0700 Subject: [PATCH 27/37] fix: cancel stripe sub when there's no ID because of reconnection --- server/src/internal/customers/cusProducts/cusProductUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts index d3d76451e..b87b8b84c 100644 --- a/server/src/internal/customers/cusProducts/cusProductUtils.ts +++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts @@ -79,7 +79,7 @@ export const cancelCusProductSubscriptions = async ({ subIds: cusProduct.subscription_ids, }); - latestSubEnd = stripeSubs[0].current_period_end; + latestSubEnd = stripeSubs?.[0]?.current_period_end; } const cancelStripeSub = async (subId: string) => { From 0500b84e8b89c617701d3279bf5dc357e55a1039 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 8 Aug 2025 19:40:49 +0100 Subject: [PATCH 28/37] adding tests --- .../scheduleUtils/ScheduleObj.ts | 1 + .../cancelScheduledFreeProduct.ts | 20 +- .../scheduleUtils/getFilteredScheduleItems.ts | 6 +- .../cusProducts/cusEnts/cusEntUtils.ts | 38 ++-- .../cusEnts/cusEntUtils/getExistingUsage.ts | 14 +- .../initCusEnt/initNextResetAt.ts | 1 + .../handlers/handleUpdateBalances.ts | 22 ++- .../internal/invoices/invoiceFormatUtils.ts | 2 + .../previewItemUtils/getCurContUseItems.ts | 1 + .../previewItemUtils/getItemsForCurProduct.ts | 1 + .../products/entitlements/entitlementUtils.ts | 27 ++- .../internal/products/prices/priceUtils.ts | 74 ++++---- .../product-items/productItemUtils.ts | 3 + .../product-items/validateProductItems.ts | 2 + server/src/internal/products/productUtils.ts | 15 +- .../productUtils/detectProductVariant.ts | 12 +- .../getProductItemDisplay.ts | 2 +- server/src/utils/scriptUtils/constructItem.ts | 7 +- .../utils/scriptUtils/createTestProducts.ts | 5 + .../utils/scriptUtils/logUtils/logSubItems.ts | 6 +- .../advanced/customInterval/customInteral2.ts | 120 ++++++++++++ .../customInterval/customInterval1 copy.ts | 164 +++++++++++++++++ .../customInterval/customInterval1.ts | 157 ++++++++++++++++ .../customInterval/customInterval3.ts | 173 ++++++++++++++++++ .../productItemModels/featureItem.ts | 1 + .../productItemModels/featurePriceItem.ts | 1 + .../productItemModels/priceItem.ts | 1 + shared/utils/intervalUtils.ts | 53 ++++++ shared/utils/productDisplayUtils.ts | 8 +- .../create-product-item/defaultItemConfigs.ts | 3 + 30 files changed, 841 insertions(+), 99 deletions(-) create mode 100644 server/tests/advanced/customInterval/customInteral2.ts create mode 100644 server/tests/advanced/customInterval/customInterval1 copy.ts create mode 100644 server/tests/advanced/customInterval/customInterval1.ts create mode 100644 server/tests/advanced/customInterval/customInterval3.ts diff --git a/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts b/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts index c5901d6f7..0dc863237 100644 --- a/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts +++ b/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts @@ -4,5 +4,6 @@ import Stripe from "stripe"; export interface ScheduleObj { schedule: Stripe.SubscriptionSchedule; interval: BillingInterval; + intervalCount: number; prices: Stripe.Price[]; } diff --git a/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts b/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts index 8d18832d3..ebba6d3c4 100644 --- a/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts +++ b/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts @@ -1,4 +1,9 @@ -import { AppEnv, FullCusProduct, Organization } from "@autumn/shared"; +import { + AppEnv, + FullCusProduct, + intervalsSame, + Organization, +} from "@autumn/shared"; import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js"; import { getScheduleIdsFromCusProducts } from "../scheduleUtils.js"; @@ -23,7 +28,7 @@ export const getOtherCusProductsOnSub = async ({ if ( cusProduct.id === curMainProduct.id || !curMainSubIds?.some((subId) => - cusProduct?.subscription_ids?.includes(subId), + cusProduct?.subscription_ids?.includes(subId) ) ) { continue; @@ -72,10 +77,13 @@ export const addCurMainProductToSchedule = async ({ }); for (const scheduleObj of schedules) { - const { schedule, interval } = scheduleObj; + const { schedule, interval, intervalCount } = scheduleObj; - let oldItemSet = oldItemSets.find( - (itemSet) => itemSet.interval === interval, + let oldItemSet = oldItemSets.find((itemSet) => + intervalsSame({ + intervalA: { interval, intervalCount }, + intervalB: itemSet, + }) ); await updateScheduledSubWithNewItems({ @@ -99,7 +107,7 @@ export const addCurMainProductToSchedule = async ({ }); logger.info( - `✅ Added old items for product ${curMainProduct.product.name} to schedule: ${schedule.id}`, + `✅ Added old items for product ${curMainProduct.product.name} to schedule: ${schedule.id}` ); } }; diff --git a/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts b/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts index d8bfaa44a..3bf93aacd 100644 --- a/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts +++ b/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts @@ -10,7 +10,7 @@ export const getFilteredScheduleItems = ({ scheduleObj: ScheduleObj; cusProducts: (FullCusProduct | undefined)[]; }) => { - const { schedule, interval, prices } = scheduleObj; + const { schedule, prices } = scheduleObj; let scheduleItems = schedule.phases[0].items; let curPrices: any[] = []; @@ -31,10 +31,10 @@ export const getFilteredScheduleItems = ({ curPrices.some( (price) => price.config?.stripe_price_id === scheduleItem.price || - price.config?.stripe_product_id === stripePrice?.product, + price.config?.stripe_product_id === stripePrice?.product ) || products.some( - (product) => product.processor?.id === stripePrice?.product, + (product) => product.processor?.id === stripePrice?.product ); return !inCurProduct; diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts index ef4659023..5497cbacb 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts @@ -5,6 +5,8 @@ import { CusProductStatus, Customer, EntInterval, + entIntervalsDifferent, + entIntervalToValue, Entitlement, EntitlementWithFeature, Entity, @@ -46,14 +48,14 @@ export const getCusEntMasterBalance = ({ (acc, curr) => { return acc + curr.balance; }, - 0, + 0 ); let totalAdjustment = Object.values(cusEnt.entities || {}).reduce( (acc, curr) => { return acc + curr.adjustment; }, - 0, + 0 ); return { @@ -69,7 +71,7 @@ export const getCusEntMasterBalance = ({ entities && entities.filter( (entity) => - entity.internal_feature_id == feature.internal_id && entity.deleted, + entity.internal_feature_id == feature.internal_id && entity.deleted ).length; return { @@ -120,7 +122,7 @@ export const sortCusEntsForDeduction = ( cusEnts: (FullCustomerEntitlement & { customer_product?: FullCusProduct; })[], - reverseOrder: boolean = false, + reverseOrder: boolean = false ) => { let intervalOrder: Record = { [EntInterval.Minute]: 0, // 1 minute @@ -206,11 +208,15 @@ export const sortCusEntsForDeduction = ( } // 3. Sort by interval - if (aEnt.interval && bEnt.interval && aEnt.interval != bEnt.interval) { + let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count); + let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count); + if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) { if (reverseOrder) { - return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval]; + return bVal.sub(aVal).toNumber(); + // return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval]; } else { - return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval]; + return aVal.sub(bVal).toNumber(); + // return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval]; } } @@ -241,7 +247,7 @@ export const sortCusEntsForDeduction = ( // Get related cusPrice export const getRelatedCusPrice = ( cusEnt: FullCustomerEntitlement, - cusPrices: FullCustomerPrice[], + cusPrices: FullCustomerPrice[] ) => { return cusPrices.find((cusPrice) => { let productMatch = @@ -328,7 +334,7 @@ export const getResetBalance = ({ return (entitlement.allowance || 0) + quantity! * billingUnits!; } catch (error) { console.log( - "WARNING: Failed to return quantity * billing units, returning allowance...", + "WARNING: Failed to return quantity * billing units, returning allowance..." ); return entitlement.allowance || 0; } @@ -347,14 +353,14 @@ export const getUnlimitedAndUsageAllowed = ({ (cusEnt) => cusEnt.internal_feature_id === internalFeatureId && (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited || - cusEnt.unlimited), + cusEnt.unlimited) ); const usageAllowed = cusEnts.some( (ent) => ent.internal_feature_id === internalFeatureId && ent.usage_allowed && - nullish(ent.entitlement.usage_limit), + nullish(ent.entitlement.usage_limit) ); return { unlimited, usageAllowed }; @@ -426,7 +432,7 @@ export const cusEntsContainFeature = ({ feature: Feature; }) => { return cusEnts.some( - (cusEnt) => cusEnt.internal_feature_id === feature.internal_id!, + (cusEnt) => cusEnt.internal_feature_id === feature.internal_id! ); }; @@ -516,7 +522,7 @@ export const getExistingUsageFromCusProducts = ({ !cp.product.is_add_on && (internalEntityId ? cp.internal_entity_id === internalEntityId - : nullish(cp.internal_entity_id)), + : nullish(cp.internal_entity_id)) ) .flatMap((cp) => cp.customer_entitlements) .find((ce) => ce.internal_feature_id === entitlement.internal_feature_id); @@ -537,15 +543,15 @@ export const getExistingUsageFromCusProducts = ({ // Get options let cusProduct = cusProducts?.find( - (cp) => cp.id === existingCusEnt.customer_product_id, + (cp) => cp.id === existingCusEnt.customer_product_id ); let options = getEntOptions( cusProduct?.options || [], - existingCusEnt.entitlement, + existingCusEnt.entitlement ); let price = getRelatedCusPrice( existingCusEnt, - cusProduct?.customer_prices || [], + cusProduct?.customer_prices || [] ); let existingAllowance = getResetBalance({ entitlement: existingCusEnt.entitlement, diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts index d9079eff5..70aa06475 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts @@ -37,7 +37,7 @@ export const getExistingCusEntAndUsage = async ({ // 1. If there is only one cus ent, return it and usage let similarCusEnts = curCusProduct.customer_entitlements.filter( - (ce) => ce.internal_feature_id === entitlement.internal_feature_id, + (ce) => ce.internal_feature_id === entitlement.internal_feature_id // && // ce.entitlement.interval === entitlement.interval ); @@ -47,8 +47,8 @@ export const getExistingCusEntAndUsage = async ({ "Similar entitlements:", similarCusEnts.map( (ce) => - `${ce.entitlement.feature_id} (${ce.entitlement.interval}) (${ce.balance})`, - ), + `${ce.entitlement.feature_id} (${ce.entitlement.interval}) (${ce.balance})` + ) ); if (similarCusEnts.length === 1) { @@ -79,7 +79,7 @@ export const getExistingUsages = ({ // Get entityUsage for (const entity of entities) { let feature = features.find( - (f) => f.internal_id === entity.internal_feature_id, + (f) => f.internal_id === entity.internal_feature_id ); let key = `${feature?.id}-${EntInterval.Lifetime}`; @@ -96,7 +96,7 @@ export const getExistingUsages = ({ for (const cusEnt of curCusProduct?.customer_entitlements || []) { let ent = cusEnt.entitlement; - let key = `${ent.feature_id}-${ent.interval}`; + let key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`; let feature = ent.feature; if (feature.type == FeatureType.Boolean) { continue; @@ -199,8 +199,8 @@ export const addExistingUsagesToCusEnts = ({ "Sorted cusEnts:", fullCusEnts.map( (ce) => - `${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}`, - ), + `${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}` + ) ); } diff --git a/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts b/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts index 1744dd900..8687ad941 100644 --- a/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts +++ b/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts @@ -62,6 +62,7 @@ export const initNextResetAt = ({ nextResetAtCalculated = getNextEntitlementReset( nextResetAtCalculated || new UTCDate(now), resetInterval, + entitlement.interval_count || 1 ).getTime(); // If anchorToUnix, align next reset at to anchorToUnix... diff --git a/server/src/internal/customers/handlers/handleUpdateBalances.ts b/server/src/internal/customers/handlers/handleUpdateBalances.ts index 278194824..23ec8dfdc 100644 --- a/server/src/internal/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/customers/handlers/handleUpdateBalances.ts @@ -147,9 +147,14 @@ export const handleUpdateBalances = async (req: any, res: any) => { continue; } + let intervalCount = cusEnt.entitlement.interval_count || 1; + let intervalCountMatch = + intervalCount > 1 ? balance.interval_count === intervalCount : true; + if ( notNullish(balance.interval) && - balance.interval !== cusEnt.entitlement.interval + balance.interval !== cusEnt.entitlement.interval && + intervalCountMatch ) { continue; } @@ -173,6 +178,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { toDeduct, properties, interval: balance.interval, + intervalCount: balance.interval_count, }); } @@ -186,12 +192,18 @@ export const handleUpdateBalances = async (req: any, res: any) => { // Handle unlimited if (featureDeduction.unlimited) { // Get one active cusEnt and set unlimited to true + const cusEnt = notNullish(interval) - ? cusEnts.find( - (cusEnt) => + ? cusEnts.find((cusEnt) => { + let cusEntIntCount = cusEnt.entitlement.interval_count || 1; + let deductionIntCount = featureDeduction.intervalCount || 1; + + return ( cusEnt.internal_feature_id === feature!.internal_id! && - cusEnt.entitlement.interval === interval - ) + cusEnt.entitlement.interval === interval && + cusEntIntCount === deductionIntCount + ); + }) : cusEnts.find( (cusEnt) => cusEnt.internal_feature_id === feature!.internal_id! ); diff --git a/server/src/internal/invoices/invoiceFormatUtils.ts b/server/src/internal/invoices/invoiceFormatUtils.ts index 646f06dcc..e190f7b71 100644 --- a/server/src/internal/invoices/invoiceFormatUtils.ts +++ b/server/src/internal/invoices/invoiceFormatUtils.ts @@ -24,6 +24,7 @@ import { } from "../customers/cusProducts/cusPrices/cusPriceUtils.js"; import { getFeatureQuantity } from "../customers/cusProducts/cusProductUtils.js"; import { formatAmount } from "@/utils/formatUtils.js"; +import { getIntervalString } from "../products/productUtils/productResponseUtils/getProductItemDisplay.js"; const getSingularAndPlural = (feature: Feature) => { const singular = getFeatureName({ @@ -72,6 +73,7 @@ export const formatFixedPrice = ({ const config = price.config as FixedPriceConfig; const amount = formatAmount({ org, amount: config.amount }); + // const intervalStr = getIntervalString({}); if (config.interval == BillingInterval.OneOff) { return `${amount}`; } else { diff --git a/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts b/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts index 64a5451ae..cc69007a6 100644 --- a/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts +++ b/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts @@ -63,6 +63,7 @@ export const getCurContUseItems = async ({ const finalProration = getProration({ now, interval: price.config.interval!, + intervalCount: price.config.interval_count || 1, anchorToUnix: sub.current_period_end * 1000, })!; diff --git a/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts b/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts index 43d678914..565637e4e 100644 --- a/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts +++ b/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts @@ -86,6 +86,7 @@ export const getItemsForCurProduct = async ({ const finalProration = getProration({ now, interval: price.config.interval!, + intervalCount: price.config.interval_count || 1, anchorToUnix: sub.current_period_end * 1000, })!; diff --git a/server/src/internal/products/entitlements/entitlementUtils.ts b/server/src/internal/products/entitlements/entitlementUtils.ts index 8c3ed276a..6d219532f 100644 --- a/server/src/internal/products/entitlements/entitlementUtils.ts +++ b/server/src/internal/products/entitlements/entitlementUtils.ts @@ -23,22 +23,28 @@ import { addDays } from "date-fns"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { features } from "process"; -export const entIntervalToTrialDuration = (interval: EntInterval) => { +export const entIntervalToTrialDuration = ({ + interval, + intervalCount, +}: { + interval: EntInterval; + intervalCount: number; +}) => { switch (interval) { case EntInterval.Day: - return 1; + return intervalCount; case EntInterval.Week: - return 7; + return intervalCount * 7; case EntInterval.Month: - return 30; + return intervalCount * 30; case EntInterval.Quarter: - return 90; + return intervalCount * 90; case EntInterval.SemiAnnual: - return 180; + return intervalCount * 180; case EntInterval.Year: - return 365; + return intervalCount * 365; case EntInterval.Lifetime: - return 1000; + return intervalCount * 1000; } }; @@ -54,7 +60,10 @@ export const applyTrialToEntitlement = ( if (entitlement.allowance_type === AllowanceType.Unlimited) return false; const trialDays = freeTrial.length; - const entDays = entIntervalToTrialDuration(entitlement.interval!); + const entDays = entIntervalToTrialDuration({ + interval: entitlement.interval!, + intervalCount: entitlement.interval_count || 1, + }); if (entDays && entDays > trialDays) { return true; diff --git a/server/src/internal/products/prices/priceUtils.ts b/server/src/internal/products/prices/priceUtils.ts index cd6875cc0..38c932c12 100644 --- a/server/src/internal/products/prices/priceUtils.ts +++ b/server/src/internal/products/prices/priceUtils.ts @@ -20,6 +20,7 @@ import { import RecaseError from "@/utils/errorUtils.js"; import { StatusCodes } from "http-status-codes"; import { Decimal } from "decimal.js"; +import { compareBillingIntervals } from "./priceUtils/priceIntervalUtils.js"; const BillingIntervalOrder = [ BillingInterval.Year, @@ -100,17 +101,30 @@ export const getBillingType = (config: FixedPriceConfig | UsagePriceConfig) => { export const getBillingInterval = (prices: Price[]) => { if (prices.length === 0) { - return BillingInterval.OneOff; + return { + interval: BillingInterval.OneOff, + intervalCount: 1, + }; } const pricesCopy = structuredClone(prices); try { pricesCopy.sort((a, b) => { - return ( - BillingIntervalOrder.indexOf(b.config!.interval!) - - BillingIntervalOrder.indexOf(a.config!.interval!) - ); + return compareBillingIntervals({ + configA: { + interval: a.config!.interval as BillingInterval, + intervalCount: a.config!.interval_count || 1, + }, + configB: { + interval: b.config!.interval as BillingInterval, + intervalCount: b.config!.interval_count || 1, + }, + }); + // return ( + // BillingIntervalOrder.indexOf(b.config!.interval!) - + // BillingIntervalOrder.indexOf(a.config!.interval!) + // ); }); } catch (error) { console.log("Error sorting prices:", error); @@ -125,7 +139,13 @@ export const getBillingInterval = (prices: Price[]) => { }); } - return pricesCopy[pricesCopy.length - 1].config!.interval as BillingInterval; + return { + interval: pricesCopy[pricesCopy.length - 1].config! + .interval as BillingInterval, + intervalCount: + pricesCopy[pricesCopy.length - 1].config!.interval_count || 1, + }; + // return pricesCopy[pricesCopy.length - 1].config!.interval as BillingInterval; }; export const pricesOnlyOneOff = (prices: Price[]) => { @@ -154,36 +174,16 @@ export const pricesContainRecurring = (prices: Price[]) => { }); }; -export const haveDifferentRecurringIntervals = (prices: Price[]) => { - let interval = null; - - for (const price of prices) { - const newInterval = price.config?.interval; - - if (newInterval == BillingInterval.OneOff) { - continue; - } - - if (interval !== null && newInterval !== null && newInterval !== interval) { - return true; - } - - interval = newInterval; - } - return false; -}; - // Get price options export const getEntOptions = ( optionsList: FeatureOptions[], - entitlement: Entitlement | EntitlementWithFeature, + entitlement: Entitlement | EntitlementWithFeature ) => { if (!entitlement) { return null; } const options = optionsList.find( - (options) => - options.internal_feature_id === entitlement.internal_feature_id, + (options) => options.internal_feature_id === entitlement.internal_feature_id ); return options; }; @@ -191,7 +191,7 @@ export const getEntOptions = ( export const getPriceEntitlement = ( price: Price, entitlements: EntitlementWithFeature[], - allowFeatureMatch = false, + allowFeatureMatch = false ) => { let config = price.config as UsagePriceConfig; @@ -217,12 +217,12 @@ export const getPriceEntitlement = ( export const getPriceOptions = ( price: Price, - optionsList: FeatureOptions[], + optionsList: FeatureOptions[] ) => { let config = price.config as UsagePriceConfig; const options = optionsList.find( - (options) => options.internal_feature_id === config.internal_feature_id, + (options) => options.internal_feature_id === config.internal_feature_id ); return options; @@ -307,7 +307,7 @@ export const getPriceForOverage = (price: Price, overage?: number) => { let amount = 0; let billingUnits = usageConfig.billing_units || 1; let remainingUsage = new Decimal( - Math.ceil(new Decimal(overage!).div(billingUnits).toNumber()), + Math.ceil(new Decimal(overage!).div(billingUnits).toNumber()) ) .mul(billingUnits) .toNumber(); @@ -353,7 +353,7 @@ export const roundPriceAmounts = (price: Price) => { const config = price.config as UsagePriceConfig; for (let i = 0; i < config.usage_tiers.length; i++) { config.usage_tiers[i].amount = Number( - config.usage_tiers[i].amount.toFixed(10), + config.usage_tiers[i].amount.toFixed(10) ); } @@ -363,7 +363,7 @@ export const roundPriceAmounts = (price: Price) => { export const priceIsOneOffAndTiered = ( price: Price, - relatedEnt: EntitlementWithFeature, + relatedEnt: EntitlementWithFeature ) => { let config = price.config as UsagePriceConfig; if (config.type == PriceType.Fixed) { @@ -371,17 +371,13 @@ export const priceIsOneOffAndTiered = ( } return ( - // (config.interval == BillingInterval.OneOff && - // config.usage_tiers.length > 0 && - // relatedEnt.allowance && - // relatedEnt.allowance > 0) || config.interval == BillingInterval.OneOff && config.usage_tiers.length > 1 ); }; export const getProductForPrice = (price: Price, products: FullProduct[]) => { return products.find( - (product) => product.internal_id === price.internal_product_id, + (product) => product.internal_id === price.internal_product_id ); }; diff --git a/server/src/internal/products/product-items/productItemUtils.ts b/server/src/internal/products/product-items/productItemUtils.ts index 00e83f41e..edd44f375 100644 --- a/server/src/internal/products/product-items/productItemUtils.ts +++ b/server/src/internal/products/product-items/productItemUtils.ts @@ -124,13 +124,16 @@ export const constructFeatureItem = ({ export const constructPriceItem = ({ price, interval, + intervalCount, }: { price: number; interval: BillingInterval | null; + intervalCount?: number; }) => { let item: ProductItem = { price: price, interval: interval as any, + interval_count: intervalCount || 1, }; return item; diff --git a/server/src/internal/products/product-items/validateProductItems.ts b/server/src/internal/products/product-items/validateProductItems.ts index 13d9214e8..e84c376bc 100644 --- a/server/src/internal/products/product-items/validateProductItems.ts +++ b/server/src/internal/products/product-items/validateProductItems.ts @@ -202,6 +202,7 @@ export const validateProductItems = ({ for (let index = 0; index < newItems.length; index++) { let item = newItems[index]; let entInterval = itemToEntInterval(item); + const intervalCount = item.interval_count || 1; if (isFeaturePriceItem(item) && entInterval == EntInterval.Lifetime) { let otherItem = newItems.find((i: any, index2: any) => { @@ -241,6 +242,7 @@ export const validateProductItems = ({ i.feature_id == item.feature_id && index2 != index && itemToEntInterval(i) == entInterval && + (i.interval_count || 1) == intervalCount && i.entity_feature_id == item.entity_feature_id ); }); diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index 4e22567e8..e6d03166b 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -8,6 +8,7 @@ import { ErrCode, Feature, FixedPriceConfig, + intervalsSame, Organization, Price, PriceSchema, @@ -141,10 +142,20 @@ export const isProductUpgrade = ({ }; // 3. Compare prices - if (billingInterval1 == billingInterval2) { + if ( + intervalsSame({ + intervalA: billingInterval1, + intervalB: billingInterval2, + }) + ) { return getTotalPrice(prices1) < getTotalPrice(prices2); } else { - return compareBillingIntervals(billingInterval1, billingInterval2) > 0; + return ( + compareBillingIntervals({ + configA: billingInterval1, + configB: billingInterval2, + }) > 0 + ); } }; diff --git a/server/src/internal/products/productUtils/detectProductVariant.ts b/server/src/internal/products/productUtils/detectProductVariant.ts index 6ec596ee0..637bdb1e2 100644 --- a/server/src/internal/products/productUtils/detectProductVariant.ts +++ b/server/src/internal/products/productUtils/detectProductVariant.ts @@ -18,9 +18,7 @@ To determine if a product is an interval variant, please follow these guidelines 1. Look at the name of the product. If it contains a word like "annual", "yearly", etc. and the name resembles another product, it's a variant. - Example of this: "Pro (Annual)" is a variant of "Pro". -2. - - +2. If the product has a similar name to another product, but interval is different, it's probably a variant. 4. If the current product is not a variant of any existing product, return null. `; @@ -62,9 +60,9 @@ export const detectBaseVariant = async ({ !p.is_add_on && p.prices.length > 0 && p.prices.every( - (price) => price.config.interval == BillingInterval.Month, + (price) => price.config.interval == BillingInterval.Month ) && - p.group == curProduct.group, + p.group == curProduct.group ); if (filteredExistingProducts.length == 0) return null; @@ -85,7 +83,7 @@ export const detectBaseVariant = async ({ id: p.id, name: p.name, prices: p.prices, - }), + }) ) .join("\n")} @@ -100,7 +98,7 @@ export const detectBaseVariant = async ({ let baseVariantId = object.base_variant_id; logger.info( - `llm response for base variant of ${curProduct.id}: ${baseVariantId}`, + `llm response for base variant of ${curProduct.id}: ${baseVariantId}` ); if (baseVariantId) { diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts index 3a4f8e212..ddb9717bf 100644 --- a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts +++ b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts @@ -18,7 +18,7 @@ import { isPriceItem, } from "../../product-items/productItemUtils/getItemType.js"; -const getIntervalString = ({ +export const getIntervalString = ({ interval, intervalCount, }: { diff --git a/server/src/utils/scriptUtils/constructItem.ts b/server/src/utils/scriptUtils/constructItem.ts index f19c8a3b3..0a7373221 100644 --- a/server/src/utils/scriptUtils/constructItem.ts +++ b/server/src/utils/scriptUtils/constructItem.ts @@ -61,6 +61,7 @@ export const constructPrepaidItem = ({ }, rolloverConfig, usageLimit, + intervalCount = 2, }: { featureId: string; price?: number; @@ -70,6 +71,7 @@ export const constructPrepaidItem = ({ config?: ProductItemConfig; rolloverConfig?: RolloverConfig; usageLimit?: number; + intervalCount?: number; }) => { let item: ProductItem = { feature_id: featureId, @@ -77,8 +79,8 @@ export const constructPrepaidItem = ({ price: price, billing_units: billingUnits || 100, - interval: isOneOff ? null : ProductItemInterval.Month, + interval_count: intervalCount, included_usage: includedUsage, config: { @@ -102,6 +104,7 @@ export const constructArrearItem = ({ }, entityFeatureId, usageLimit, + intervalCount = 1, }: { featureId: string; includedUsage?: number; @@ -110,6 +113,7 @@ export const constructArrearItem = ({ config?: ProductItemConfig; entityFeatureId?: string; usageLimit?: number; + intervalCount?: number; }) => { let item: ProductItem = { feature_id: featureId, @@ -118,6 +122,7 @@ export const constructArrearItem = ({ price: price, billing_units: billingUnits, interval: ProductItemInterval.Month, + interval_count: intervalCount, reset_usage_when_enabled: true, config, entity_feature_id: entityFeatureId, diff --git a/server/src/utils/scriptUtils/createTestProducts.ts b/server/src/utils/scriptUtils/createTestProducts.ts index 268ab394c..fb1bd26cf 100644 --- a/server/src/utils/scriptUtils/createTestProducts.ts +++ b/server/src/utils/scriptUtils/createTestProducts.ts @@ -74,13 +74,16 @@ export const constructRawProduct = ({ is_default: false, version: 1, group: "", + created_at: Date.now(), }; }; + export const constructProduct = ({ id, items, type, interval, + intervalCount, isAnnual = false, trial = false, excludeBase = false, @@ -92,6 +95,7 @@ export const constructProduct = ({ items: ProductItem[]; type: "free" | "pro" | "premium" | "growth" | "one_off"; interval?: BillingInterval; + intervalCount?: number; isAnnual?: boolean; trial?: boolean; excludeBase?: boolean; @@ -117,6 +121,7 @@ export const constructProduct = ({ : interval ? interval : BillingInterval.Month, + intervalCount: intervalCount || 1, }) ); } diff --git a/server/src/utils/scriptUtils/logUtils/logSubItems.ts b/server/src/utils/scriptUtils/logUtils/logSubItems.ts index 679087de7..f5d443661 100644 --- a/server/src/utils/scriptUtils/logUtils/logSubItems.ts +++ b/server/src/utils/scriptUtils/logUtils/logSubItems.ts @@ -17,8 +17,10 @@ export const logSubItems = ({ console.log(`Usage price`); } else { let price = item.price.unit_amount! / 100; - let interval = subItemToAutumnInterval(item); - console.log(`${price} ${item.price.currency} / ${interval}`); + let subInterval = subItemToAutumnInterval(item); + console.log( + `${price} ${item.price.currency} / ${subInterval?.interval} (${subInterval?.intervalCount})` + ); } } }; diff --git a/server/tests/advanced/customInterval/customInteral2.ts b/server/tests/advanced/customInterval/customInteral2.ts new file mode 100644 index 000000000..9b39c0d9f --- /dev/null +++ b/server/tests/advanced/customInterval/customInteral2.ts @@ -0,0 +1,120 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addHours, addMonths } from "date-fns"; +import { expect } from "chai"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; + +const testCase = "customInterval2"; + +export let pro = constructRawProduct({ + id: "pro", + items: [ + constructArrearItem({ + includedUsage: 0, + featureId: TestFeature.Words, + intervalCount: 2, + }), + ], +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach pro product", async function () { + await runAttachTest({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + }); + + let usage = 100012; + it("should upgrade to premium product and have correct invoice next cycle", async function () { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + + const curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 2), + hoursToFinalizeInvoice + ).getTime(), + waitForSeconds: 30, + }); + + const invoiceAmount = await getExpectedInvoiceTotal({ + customerId, + productId: pro.id, + usage: [{ featureId: TestFeature.Words, value: usage }], + stripeCli, + db, + org, + env, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices.length).to.equal(2); + expect(invoiceAmount).to.equal(customer.invoices[0].total); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval1 copy.ts b/server/tests/advanced/customInterval/customInterval1 copy.ts new file mode 100644 index 000000000..6d2a4e673 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval1 copy.ts @@ -0,0 +1,164 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { + constructArrearItem, + constructArrearProratedItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { timeout } from "@/utils/genUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; +import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; + +const testCase = "upgrade6"; + +export let pro = constructProduct({ + items: [ + constructArrearItem({ featureId: TestFeature.Words }), + constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 20, + }), + ], + type: "pro", +}); + +export let premium = constructProduct({ + items: [ + constructArrearItem({ featureId: TestFeature.Words }), + constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 30, + }), + ], + type: "premium", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro, premium], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach pro product", async function () { + await runAttachTest({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + }); + + let usage = 100012; + it("should upgrade to premium product and fail", async function () { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + await timeout(4000); + + let cus = await CusService.get({ + db, + orgId: org.id, + idOrInternalId: customerId, + env, + }); + + await attachFailedPaymentMethod({ stripeCli, customer: cus! }); + await timeout(2000); + + await expectAutumnError({ + func: async () => { + await runAttachTest({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + }); + }, + errMessage: "Failed to update subscription. Your card was declined.", + }); + + await timeout(4000); + let customer = await autumn.customers.get(customerId); + + expectProductAttached({ + customer, + product: pro, + }); + + expectFeaturesCorrect({ + customer, + product: pro, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + + await expectSubItemsCorrect({ + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval1.ts b/server/tests/advanced/customInterval/customInterval1.ts new file mode 100644 index 000000000..360d9e161 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval1.ts @@ -0,0 +1,157 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +import { + constructArrearItem, + constructArrearProratedItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { timeout } from "@/utils/genUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; +import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addHours, addMonths } from "date-fns"; +import { expect } from "chai"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; + +const testCase = "customInterval1"; + +export let pro = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + intervalCount: 200, + }), + // constructArrearItem({ featureId: TestFeature.Words }), + // constructArrearProratedItem({ + // featureId: TestFeature.Users, + // pricePerUnit: 20, + // }), + ], + intervalCount: 2, + type: "pro", +}); + +export let premium = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + intervalCount: 500, + }), + // constructArrearItem({ featureId: TestFeature.Words }), + // constructArrearProratedItem({ + // featureId: TestFeature.Users, + // pricePerUnit: 30, + // }), + ], + intervalCount: 2, + type: "premium", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro, premium], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach pro product", async function () { + await runAttachTest({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + }); + + let usage = 100012; + it("should upgrade to premium product and have correct invoice next cycle", async function () { + const curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addMonths(new Date(), 1).getTime(), + waitForSeconds: 15, + }); + + await runAttachTest({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices.length).to.equal(2); + + const nextUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(curUnix), 1), + hoursToFinalizeInvoice + ).getTime(), + waitForSeconds: 30, + }); + + const customer2 = await autumn.customers.get(customerId); + const invoices = customer2.invoices; + expect(invoices.length).to.equal(3); + expect(invoices[0].product_ids).to.include(premium.id); + expect(invoices[0].total).to.equal(getBasePrice({ product: premium })); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval3.ts b/server/tests/advanced/customInterval/customInterval3.ts new file mode 100644 index 000000000..ea89242f8 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval3.ts @@ -0,0 +1,173 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { + APIVersion, + AppEnv, + BillingInterval, + LimitedItem, + Organization, + Product, +} from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +import { + constructArrearItem, + constructFeatureItem, + constructPrepaidItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { + constructProduct, + constructRawProduct, +} from "@/utils/scriptUtils/createTestProducts.js"; + +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addDays, addHours, addMonths } from "date-fns"; +import { expect } from "chai"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js"; + +const testCase = "customInterval3"; + +export let pro = constructProduct({ + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + intervalCount: 2, + }), + ], + intervalCount: 2, +}); + +const prepaidWordsItem = constructPrepaidItem({ + featureId: TestFeature.Words, + price: 10, + billingUnits: 1, + includedUsage: 0, +}); + +export const addOn = constructRawProduct({ + id: "addOn", + items: [prepaidWordsItem], + isAddOn: true, +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro, addOn], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro, addOn], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach pro product", async function () { + await runAttachTest({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + }); + + it("should upgrade to premium product and have correct invoice next cycle", async function () { + const curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addDays(new Date(), 20).getTime(), + waitForSeconds: 15, + }); + + const wordBillingSets = 2; + const wordsBillingUnits = prepaidWordsItem.billing_units! * wordBillingSets; + await autumn.attach({ + customer_id: customerId, + product_id: addOn.id, + options: [ + { + feature_id: TestFeature.Words, + quantity: wordsBillingUnits, + }, + ], + }); + + const customer = await autumn.customers.get(customerId); + const proProduct = customer.products.find((p) => p.id === pro.id); + const invoices = customer.invoices; + expectProductAttached({ + customer, + product: pro, + }); + + expectProductAttached({ + customer, + product: addOn, + }); + + let expectedPrice = wordsBillingUnits * prepaidWordsItem.price!; + expect(invoices[0].product_ids).to.include(addOn.id); + expect(invoices[0].total).to.approximately( + calculateProrationAmount({ + amount: expectedPrice, + periodStart: curUnix!, + periodEnd: addMonths(curUnix!, 1).getTime(), + now: curUnix!, + }), + 0.1 + ); + + const expectedAddonEnd = addMonths(curUnix, 1); + const approximate = 1000 * 60 * 60 * 24; // +- 1 day + const addOnProduct = customer.products.find((p) => p.id === addOn.id); + + expect(addOnProduct?.current_period_end).to.be.closeTo( + expectedAddonEnd.getTime(), + approximate + ); + }); +}); diff --git a/shared/models/productV2Models/productItemModels/featureItem.ts b/shared/models/productV2Models/productItemModels/featureItem.ts index 3ce838cba..1ca38afdd 100644 --- a/shared/models/productV2Models/productItemModels/featureItem.ts +++ b/shared/models/productV2Models/productItemModels/featureItem.ts @@ -7,6 +7,7 @@ export const FeatureItemSchema = ProductItemSchema.pick({ feature_type: true, included_usage: true, interval: true, + interval_count: true, entity_feature_id: true, reset_usage_when_enabled: true, config: true, diff --git a/shared/models/productV2Models/productItemModels/featurePriceItem.ts b/shared/models/productV2Models/productItemModels/featurePriceItem.ts index 63eb4a780..93391f973 100644 --- a/shared/models/productV2Models/productItemModels/featurePriceItem.ts +++ b/shared/models/productV2Models/productItemModels/featurePriceItem.ts @@ -6,6 +6,7 @@ export const FeaturePriceItemSchema = ProductItemSchema.pick({ feature_type: true, included_usage: true, interval: true, + interval_count: true, usage_model: true, price: true, diff --git a/shared/models/productV2Models/productItemModels/priceItem.ts b/shared/models/productV2Models/productItemModels/priceItem.ts index a5bf44199..db075b3b2 100644 --- a/shared/models/productV2Models/productItemModels/priceItem.ts +++ b/shared/models/productV2Models/productItemModels/priceItem.ts @@ -4,6 +4,7 @@ import { z } from "zod"; export const PriceItemSchema = ProductItemSchema.pick({ price: true, interval: true, + interval_count: true, }).extend({ price: z.number().nonnegative(), }); diff --git a/shared/utils/intervalUtils.ts b/shared/utils/intervalUtils.ts index 15fb535e2..fb88ce05f 100644 --- a/shared/utils/intervalUtils.ts +++ b/shared/utils/intervalUtils.ts @@ -1,3 +1,5 @@ +import { Decimal } from "decimal.js"; +import { EntInterval } from "../models/productModels/entModels/entEnums.js"; import { BillingInterval } from "../models/productModels/priceModels/priceEnums.js"; export const intervalToValue = ( @@ -41,3 +43,54 @@ export const intervalsSame = ({ }) => { return !intervalsDifferent({ intervalA, intervalB }); }; + +type EntIntervalConfig = { + interval: EntInterval; + intervalCount?: number | null; +}; + +export const entIntervalToValue = ( + interval?: EntInterval | null, + intervalCount?: number | null +) => { + if (!interval) { + return new Decimal(10000000); + } + + const intervalToBaseVal: Record = { + [EntInterval.Minute]: 1, + [EntInterval.Hour]: 60, + [EntInterval.Day]: 1 * 60 * 24, + [EntInterval.Week]: 1 * 60 * 24 * 7, + [EntInterval.Month]: 1 * 60 * 24 * 30, + [EntInterval.Quarter]: 1 * 60 * 24 * 90, + [EntInterval.SemiAnnual]: 1 * 60 * 24 * 180, + [EntInterval.Year]: 1 * 60 * 24 * 365, + [EntInterval.Lifetime]: 1000000000, + }; + + const baseValue = intervalToBaseVal[interval]; + return new Decimal(baseValue).mul(intervalCount ?? 1); +}; + +export const entIntervalsSame = ({ + intervalA, + intervalB, +}: { + intervalA: EntIntervalConfig; + intervalB: EntIntervalConfig; +}) => { + const valA = entIntervalToValue(intervalA.interval, intervalA.intervalCount); + const valB = entIntervalToValue(intervalB.interval, intervalB.intervalCount); + return valA.eq(valB); +}; + +export const entIntervalsDifferent = ({ + intervalA, + intervalB, +}: { + intervalA: EntIntervalConfig; + intervalB: EntIntervalConfig; +}) => { + return !entIntervalsSame({ intervalA, intervalB }); +}; diff --git a/shared/utils/productDisplayUtils.ts b/shared/utils/productDisplayUtils.ts index 692cbda58..ef1d1f2f1 100644 --- a/shared/utils/productDisplayUtils.ts +++ b/shared/utils/productDisplayUtils.ts @@ -109,7 +109,13 @@ export const getPriceItemDisplay = ({ currency, amount: item.price as number, }); - let secondaryText = item.interval ? `per ${item.interval}` : undefined; + + const intervalStr = getIntervalString({ + interval: item.interval!, + intervalCount: item.interval_count, + }); + + let secondaryText = intervalStr || undefined; return { primary_text: primaryText, diff --git a/vite/src/views/products/product/product-item/create-product-item/defaultItemConfigs.ts b/vite/src/views/products/product/product-item/create-product-item/defaultItemConfigs.ts index f206ad400..2628d2a81 100644 --- a/vite/src/views/products/product/product-item/create-product-item/defaultItemConfigs.ts +++ b/vite/src/views/products/product/product-item/create-product-item/defaultItemConfigs.ts @@ -6,6 +6,7 @@ export const defaultFeatureItem: ProductItem = { included_usage: null, interval: ProductItemInterval.Month, + interval_count: 1, // Price config price: null, @@ -21,6 +22,7 @@ export const defaultPaidFeatureItem: ProductItem = { feature_id: null, included_usage: null, interval: ProductItemInterval.Month, + interval_count: 1, // Price config price: null, @@ -42,6 +44,7 @@ export const defaultPriceItem: ProductItem = { included_usage: null, interval: ProductItemInterval.Month, + interval_count: 1, // Price config price: 0, From c4d1d553ebfc55b5cf1b7438a5d584fcd241edd0 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 9 Aug 2025 07:55:53 +0100 Subject: [PATCH 29/37] fix: UI issues and cancel product that's already cancelled --- .../external/stripe/stripeOnboardingUtils.ts | 3 +- .../internal/customers/expire/expireRouter.ts | 25 ---------- .../handlers/handleCusProductExpired.ts | 46 +++++++++++++++---- .../orgs/handlers/handleConnectStripe.ts | 7 ++- server/src/internal/orgs/orgRouter.ts | 10 ++++ .../components/general/PageSectionHeader.tsx | 6 ++- .../CancelProductDialog.tsx | 4 +- vite/src/views/features/CreateFeature.tsx | 4 +- vite/src/views/products/CreateProduct.tsx | 4 +- vite/src/views/products/ProductsView.tsx | 41 ++++++++--------- .../components/SelectItemFeature.tsx | 1 + 11 files changed, 88 insertions(+), 63 deletions(-) diff --git a/server/src/external/stripe/stripeOnboardingUtils.ts b/server/src/external/stripe/stripeOnboardingUtils.ts index 4e14dfe1b..207b864fd 100644 --- a/server/src/external/stripe/stripeOnboardingUtils.ts +++ b/server/src/external/stripe/stripeOnboardingUtils.ts @@ -7,6 +7,7 @@ export const checkKeyValid = async (apiKey: string) => { // Call customers.list const customers = await stripe.customers.list(); + // const account = await stripe.accounts.retrieve(); // console.log("Account", account); // return account; @@ -15,7 +16,7 @@ export const checkKeyValid = async (apiKey: string) => { export const createWebhookEndpoint = async ( apiKey: string, env: AppEnv, - orgId: string, + orgId: string ) => { const stripe = new Stripe(apiKey); diff --git a/server/src/internal/customers/expire/expireRouter.ts b/server/src/internal/customers/expire/expireRouter.ts index ad6a76ab7..4dd18dd61 100644 --- a/server/src/internal/customers/expire/expireRouter.ts +++ b/server/src/internal/customers/expire/expireRouter.ts @@ -65,31 +65,6 @@ expireRouter.post("", async (req, res) => prorate, }); - // console.log( - // "CUs products to expire", - // cusProductsToExpire.map((c) => c.product.id) - // ); - // throw new Error("test"); - - // if (!cusProductsToExpire) { - // throw new RecaseError({ - // code: ErrCode.ProductNotFound, - // message: `Product ${product_id} not found for customer ${customer_id}`, - // }); - // } - - // // Handle case if there are two products to expire... - - // for (const cusProduct of cusProductsToExpire) { - // await expireCusProduct({ - // req, - // cusProduct, - // fullCus, - // expireImmediately, - // prorate, - // }); - // } - res.status(200).json({ success: true, customer_id: customer_id, diff --git a/server/src/internal/customers/handlers/handleCusProductExpired.ts b/server/src/internal/customers/handlers/handleCusProductExpired.ts index 69c0372f2..c1c71039c 100644 --- a/server/src/internal/customers/handlers/handleCusProductExpired.ts +++ b/server/src/internal/customers/handlers/handleCusProductExpired.ts @@ -2,13 +2,16 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { + ACTIVE_STATUSES, + CusProductService, +} from "@/internal/customers/cusProducts/CusProductService.js"; import { cancelCusProductSubscriptions, expireAndActivate, fullCusProductToProduct, } from "@/internal/customers/cusProducts/cusProductUtils.js"; -import { isOneOff } from "@/internal/products/productUtils.js"; +import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; import { @@ -21,6 +24,7 @@ import { } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import { CusService } from "../CusService.js"; +import { cusProductToPrices } from "../cusProducts/cusProductUtils/convertCusProduct.js"; export const removeScheduledProduct = async ({ req, @@ -112,14 +116,29 @@ export const expireCusProduct = async ({ // 1. If main product, can't expire if there's scheduled product let isMain = !cusProduct.product.is_add_on; - if (isMain) { - let { curScheduledProduct: futureProduct } = getExistingCusProducts({ - product: cusProduct.product, - cusProducts: fullCus.customer_products, - internalEntityId: cusProduct.internal_entity_id, - }); + let { curScheduledProduct: futureProduct } = getExistingCusProducts({ + product: cusProduct.product, + cusProducts: fullCus.customer_products, + internalEntityId: cusProduct.internal_entity_id, + }); - if (futureProduct) { + if (isMain) { + if ( + cusProduct.canceled_at && + ACTIVE_STATUSES.includes(cusProduct.status) && + !expireImmediately + ) { + throw new RecaseError({ + message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`, + code: ErrCode.InvalidRequest, + statusCode: StatusCodes.BAD_REQUEST, + }); + } + + if ( + futureProduct && + !isFreeProduct(cusProductToPrices({ cusProduct: futureProduct })) + ) { throw new RecaseError({ message: `Please delete scheduled product ${futureProduct.product.name} first`, code: ErrCode.InvalidRequest, @@ -130,6 +149,7 @@ export const expireCusProduct = async ({ // 2. If expire at cycle end, just cancel subscriptions if (!expireImmediately) { + // 1. Check if already canceled await cancelCusProductSubscriptions({ cusProduct, org, @@ -175,6 +195,14 @@ export const expireCusProduct = async ({ return; } + // Remove scheduled products first... + if (futureProduct) { + await CusProductService.delete({ + db, + cusProductId: futureProduct.id, + }); + } + logger.info(`Expiring current product: ${cusProduct.product.name}`); await expireAndActivate({ req, diff --git a/server/src/internal/orgs/handlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/handleConnectStripe.ts index 4d46b8494..7369860b5 100644 --- a/server/src/internal/orgs/handlers/handleConnectStripe.ts +++ b/server/src/internal/orgs/handlers/handleConnectStripe.ts @@ -32,9 +32,11 @@ export const connectStripe = async ({ env: AppEnv; }) => { // 1. Check if key is valid + await checkKeyValid(apiKey); let stripe = new Stripe(apiKey); + let account = await stripe.accounts.retrieve(); // 2. Disconnect existing webhook endpoints @@ -47,6 +49,7 @@ export const connectStripe = async ({ // 3. Create webhook endpoint let webhook = await createWebhookEndpoint(apiKey, env, orgId); + console.log("MADE IT HERE"); // 3. Return encrypted if (env === AppEnv.Sandbox) { @@ -96,7 +99,9 @@ export const connectAllStripe = async ({ // Get default currency from Stripe let stripe = new Stripe(testApiKey); + let account = await stripe.accounts.retrieve(); + if (nullish(defaultCurrency) && nullish(account.default_currency)) { throw new RecaseError({ message: "Default currency not set", @@ -107,7 +112,7 @@ export const connectAllStripe = async ({ defaultCurrency = account.default_currency; } } catch (error: any) { - console.error("Error checking stripe keys", error); + // console.error("Error checking stripe keys", error); throw new RecaseError({ message: error.message || "Invalid Stripe API keys", code: ErrCode.StripeKeyInvalid, diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts index 18a6ebef8..37bfc73ab 100644 --- a/server/src/internal/orgs/orgRouter.ts +++ b/server/src/internal/orgs/orgRouter.ts @@ -77,12 +77,14 @@ orgRouter.post("/stripe", async (req: any, res) => { logger, }); + console.log("Connecting Stripe"); await checkKeyValid(testApiKey); await checkKeyValid(liveApiKey); // Get default currency from Stripe let stripe = new Stripe(testApiKey); let account = await stripe.accounts.retrieve(); + if (nullish(defaultCurrency) && nullish(account.default_currency)) { throw new RecaseError({ message: "Default currency not set", @@ -93,6 +95,14 @@ orgRouter.post("/stripe", async (req: any, res) => { defaultCurrency = account.default_currency; } } catch (error: any) { + // if (error.message.includes("rk_***")) { + // throw new RecaseError({ + // message: "Invalid Stripe restricted key. Please add the ", + // code: ErrCode.StripeKeyInvalid, + // statusCode: 500, + // }); + // } + throw new RecaseError({ message: error.message || "Invalid Stripe API keys", code: ErrCode.StripeKeyInvalid, diff --git a/vite/src/components/general/PageSectionHeader.tsx b/vite/src/components/general/PageSectionHeader.tsx index 1f73a23c4..03e863e8d 100644 --- a/vite/src/components/general/PageSectionHeader.tsx +++ b/vite/src/components/general/PageSectionHeader.tsx @@ -38,9 +38,11 @@ export const PageSectionHeader = ({ )} {titleComponent}
-
+
{endContent} - {addButton &&
{addButton}
} + {addButton && ( +
{addButton}
+ )} {menuComponent && (
{menuComponent}
)} diff --git a/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx b/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx index 7ceab2e32..8c04c398a 100644 --- a/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx +++ b/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx @@ -12,7 +12,7 @@ import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils"; import { useState } from "react"; import { toast } from "sonner"; import { useCustomerContext } from "../CustomerContext"; -import { notNullish } from "@/utils/genUtils"; +import { getBackendErr, notNullish } from "@/utils/genUtils"; export const CancelProductDialog = ({ cusProduct, @@ -50,7 +50,7 @@ export const CancelProductDialog = ({ setOpen(false); toast.success("Product cancelled"); } catch (error) { - toast.error("Failed to cancel product"); + toast.error(getBackendErr(error, "Failed to cancel product")); } finally { if (cancelImmediately) { setImmediateLoading(false); diff --git a/vite/src/views/features/CreateFeature.tsx b/vite/src/views/features/CreateFeature.tsx index 1cb417374..ef07f98a6 100644 --- a/vite/src/views/features/CreateFeature.tsx +++ b/vite/src/views/features/CreateFeature.tsx @@ -142,7 +142,9 @@ export const CreateFeatureDialog = () => { return ( - + diff --git a/vite/src/views/products/CreateProduct.tsx b/vite/src/views/products/CreateProduct.tsx index 4f6eae52b..b2baa1b15 100644 --- a/vite/src/views/products/CreateProduct.tsx +++ b/vite/src/views/products/CreateProduct.tsx @@ -77,7 +77,9 @@ function CreateProduct({ return ( - + Create Product diff --git a/vite/src/views/products/ProductsView.tsx b/vite/src/views/products/ProductsView.tsx index 77041fbd5..cfaf9c38f 100644 --- a/vite/src/views/products/ProductsView.tsx +++ b/vite/src/views/products/ProductsView.tsx @@ -141,11 +141,7 @@ function ProductsView({ env }: { env: AppEnv }) { )} } - addButton={ - <> - - - } + addButton={} menuComponent={ -
-
-

Features

- - {featuresData?.features?.length || 0} - - {showArchivedFeatures && ( - - Archived - - )} -
-
- + + + {featuresData?.features?.length} + + {showArchived && ( + + Archived + + )} + + } + addButton={} + menuComponent={ -
-
+ } + /> +
diff --git a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx index f73b0215b..2713cb19d 100644 --- a/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx +++ b/vite/src/views/products/product/product-item/components/SelectItemFeature.tsx @@ -51,6 +51,7 @@ export const SelectItemFeature = ({ {features .filter((feature: Feature) => { + if (feature.archived) return false; if (itemType === ProductItemType.FeaturePrice) { return feature.type !== FeatureType.Boolean; } From 23771a5301670c2bcfa1dec9aab7706c8f89369f Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 9 Aug 2025 08:19:02 +0100 Subject: [PATCH 30/37] fix: race condition in free cont use feature --- server/src/internal/api/events/usageRouter.ts | 6 ++-- server/src/internal/features/featureUtils.ts | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/server/src/internal/api/events/usageRouter.ts b/server/src/internal/api/events/usageRouter.ts index f05f3a5b2..7af89e419 100644 --- a/server/src/internal/api/events/usageRouter.ts +++ b/server/src/internal/api/events/usageRouter.ts @@ -21,6 +21,7 @@ import { getEventTimestamp } from "./eventUtils.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; import { logger } from "@/external/logtail/logtailUtils.js"; +import { isPaidContinuousUse } from "@/internal/features/featureUtils.js"; export const eventsRouter: Router = Router(); export const usageRouter: Router = Router(); @@ -201,9 +202,8 @@ export const handleUsageEvent = async ({ entityId: entity_id, }; - const featureUsageType = feature.config?.usage_type; - // console.log(`Feature Usage Type: ${featureUsageType}`); - if (featureUsageType === FeatureUsageType.Continuous) { + if (isPaidContinuousUse({ feature, fullCus: customer })) { + console.log(`Running update usage task synchronously`); await runUpdateUsageTask({ payload, logger: console, diff --git a/server/src/internal/features/featureUtils.ts b/server/src/internal/features/featureUtils.ts index b65a36bae..b9d6aee1d 100644 --- a/server/src/internal/features/featureUtils.ts +++ b/server/src/internal/features/featureUtils.ts @@ -9,6 +9,8 @@ import { Organization, ProductItemFeatureType, FeatureType, + FullCustomer, + UsagePriceConfig, } from "@autumn/shared"; import { FeatureService } from "./FeatureService.js"; import { StatusCodes } from "http-status-codes"; @@ -16,6 +18,11 @@ import { generateFeatureDisplay } from "@/external/llm/llmUtils.js"; import { ProductService } from "../products/ProductService.js"; import { getCreditSystemsFromFeature } from "./creditSystemUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { + cusProductsToCusPrices, + cusProductToPrices, +} from "../customers/cusProducts/cusProductUtils/convertCusProduct.js"; +import { priceToFeature } from "../products/prices/priceUtils/convertPrice.js"; export const validateFeatureId = (featureId: string) => { if (!featureId.match(/^[a-zA-Z0-9_-]+$/)) { @@ -204,3 +211,29 @@ export const getCusFeatureType = ({ feature }: { feature: Feature }) => { export const isCreditSystem = ({ feature }: { feature: Feature }) => { return feature.type == FeatureType.CreditSystem; }; + +export const isPaidContinuousUse = ({ + feature, + fullCus, +}: { + feature: Feature; + fullCus: FullCustomer; +}) => { + let isContinuous = feature.config?.usage_type == FeatureUsageType.Continuous; + + if (!isContinuous) { + return false; + } + + let cusPrices = cusProductsToCusPrices({ + cusProducts: fullCus.customer_products, + }); + let hasPaid = cusPrices.some((cp) => { + let config = cp.price.config as UsagePriceConfig; + if (config.internal_feature_id == feature.internal_id) { + return true; + } + }); + + return hasPaid; +}; From d8874d9dadac67c19a07bd2d414ab97a45588f81 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 10 Aug 2025 07:15:16 +0100 Subject: [PATCH 31/37] tests: custom intervals --- server/shell/g4.sh | 3 +- server/src/external/autumn/autumnCli.ts | 12 + .../src/external/stripe/stripePriceUtils.ts | 5 + .../attach/attachUtils/getAttachBranch.ts | 1 + .../change-product/billRemainingUsages.ts | 410 ------------------ .../customers/cusUtils/createNewCustomer.ts | 7 +- .../balancesToFeatureResponse.ts | 3 + .../cusFeatureResponseUtils/getCusBalances.ts | 1 + .../handlers/handleUpdateBalances.ts | 36 +- .../products/prices/billingIntervalUtils.ts | 8 + .../internal/products/prices/priceUtils.ts | 14 +- .../prices/priceUtils/convertPrice.ts | 6 +- .../prices/priceUtils/priceIntervalUtils.ts | 1 + server/src/internal/products/productUtils.ts | 5 +- .../utils/importUtils/addProductFromSubs.ts | 12 +- server/src/utils/scriptUtils/constructItem.ts | 2 +- .../customInterval/customInterval1 copy.ts | 164 ------- .../customInterval/customInterval1.ts | 37 +- .../{customInteral2.ts => customInterval2.ts} | 0 .../customInterval/customInterval3.ts | 28 +- .../customInterval/customInterval4.ts | 150 +++++++ .../customInterval/customInterval5.ts | 163 +++++++ .../customInterval/customInterval6.ts | 165 +++++++ server/tests/utils/productUtils.ts | 10 +- .../productModels/priceModels/priceEnums.ts | 1 + shared/utils/intervalUtils.ts | 1 + 26 files changed, 600 insertions(+), 645 deletions(-) delete mode 100644 server/src/internal/customers/change-product/billRemainingUsages.ts delete mode 100644 server/tests/advanced/customInterval/customInterval1 copy.ts rename server/tests/advanced/customInterval/{customInteral2.ts => customInterval2.ts} (100%) create mode 100644 server/tests/advanced/customInterval/customInterval4.ts create mode 100644 server/tests/advanced/customInterval/customInterval5.ts create mode 100644 server/tests/advanced/customInterval/customInterval6.ts diff --git a/server/shell/g4.sh b/server/shell/g4.sh index 654df5cae..75aa8212b 100755 --- a/server/shell/g4.sh +++ b/server/shell/g4.sh @@ -12,7 +12,8 @@ $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ 'tests/advanced/coupons/*.ts' \ 'tests/attach/updateQuantity/*.ts' \ 'tests/advanced/referrals/*.ts' \ - 'tests/advanced/rollovers/*.ts' + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' # $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ # 'tests/advanced/usageLimit/*.ts' diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 92922a3ed..0557c6ad2 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -12,6 +12,8 @@ import { } from "@autumn/shared"; import { CancelParams, + CheckoutParams, + CheckoutResult, CheckParams, CheckResult, Customer, @@ -168,6 +170,16 @@ export class AutumnInt { return data; } + async checkout(params: CheckoutParams) { + // const data = await this.post(`/attach`, { + // customer_id: customerId, + // product_id: productId, + // options: toSnakeCase(options), + // }); + const data = await this.post(`/checkout`, params); + + return data as CheckoutResult; + } async sendEvent({ customerId, diff --git a/server/src/external/stripe/stripePriceUtils.ts b/server/src/external/stripe/stripePriceUtils.ts index 4a7f5e3d5..ccbc811bf 100644 --- a/server/src/external/stripe/stripePriceUtils.ts +++ b/server/src/external/stripe/stripePriceUtils.ts @@ -32,6 +32,11 @@ export const billingIntervalToStripe = ({ }) => { const finalCount = intervalCount ?? 1; switch (interval) { + case BillingInterval.Week: + return { + interval: "week", + interval_count: finalCount, + }; case BillingInterval.Month: return { interval: "month", diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts index d40ca6fe0..526745120 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts @@ -273,6 +273,7 @@ const getChangeProductBranch = async ({ // } let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices }); + if (isUpgrade) { if (isTrialing(curMainProduct!)) { return AttachBranch.MainIsTrial; diff --git a/server/src/internal/customers/change-product/billRemainingUsages.ts b/server/src/internal/customers/change-product/billRemainingUsages.ts deleted file mode 100644 index ae69888b5..000000000 --- a/server/src/internal/customers/change-product/billRemainingUsages.ts +++ /dev/null @@ -1,410 +0,0 @@ -import { Stripe } from "stripe"; -import { AttachParams } from "../cusProducts/AttachParams.js"; -import { FullCusProduct, InvoiceItem } from "@autumn/shared"; -import { BillingInterval, BillingType, UsagePriceConfig } from "@autumn/shared"; - -import { - getBillingType, - getPriceEntitlement, - getPriceForOverage, -} from "@/internal/products/prices/priceUtils.js"; -import { - createStripeCli, - subToAutumnInterval, -} from "@/external/stripe/utils.js"; -import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js"; -import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js"; -import { getResetBalancesUpdate } from "../cusProducts/cusEnts/groupByUtils.js"; -import { - getCusPriceUsage, - getRelatedCusEnt, -} from "../cusProducts/cusPrices/cusPriceUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; -import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; -import { - cusProductsToCusPrices, - cusProductToEnts, -} from "../cusProducts/cusProductUtils/convertCusProduct.js"; -import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js"; - -// Add usage to end of cycle -// const addUsageToNextInvoice = async ({ -// db, -// intervalToInvoiceItems, -// intervalToSub, -// customer, -// org, -// logger, -// attachParams, -// }: { -// db: DrizzleCli; -// intervalToInvoiceItems: any; -// intervalToSub: any; -// customer: any; -// org: any; -// logger: any; -// attachParams: AttachParams; -// }) => { -// for (const interval in intervalToInvoiceItems) { -// const itemsToInvoice = intervalToInvoiceItems[interval]; - -// if (itemsToInvoice.length === 0) { -// continue; -// } - -// // Add items to invoice -// const stripeCli = createStripeCli({ -// org: org, -// env: customer.env, -// }); - -// for (const item of itemsToInvoice) { -// const { amount, description } = item; - -// logger.info( -// ` feature: ${item.feature.id}, overage: ${item.overage}, amount: ${amount}`, -// ); - -// let relatedSub = intervalToSub[interval]; -// if (!relatedSub) { -// continue; -// } - -// // Create invoice item -// let invoiceItem = { -// customer: customer.processor.id, -// currency: org.default_currency, -// description, -// price_data: { -// product: (item.price.config! as UsagePriceConfig).stripe_product_id!, -// unit_amount: Math.round(amount * 100), -// currency: org.default_currency, -// }, -// subscription: relatedSub.id, -// period: { -// start: item.periodStart, -// end: item.periodEnd, -// }, -// }; - -// await stripeCli.invoiceItems.create(invoiceItem); - -// // Update cus ent to 0 -// await CusEntService.update({ -// db, -// id: item.relatedCusEnt!.id, -// updates: getResetBalancesUpdate({ -// cusEnt: item.relatedCusEnt!, -// allowance: 0, -// }), -// }); - -// // Update existing cusEnt in attachParams -// let cusProducts = attachParams.cusProducts; -// for (const cusProduct of cusProducts!) { -// for (let i = 0; i < cusProduct.customer_entitlements.length; i++) { -// let cusEnt = cusProduct.customer_entitlements[i]; -// if (cusEnt.id === item.relatedCusEnt!.id) { -// let balancesUpdate = getResetBalancesUpdate({ -// cusEnt, -// allowance: 0, -// }); -// cusProduct.customer_entitlements[i] = { -// ...cusEnt, -// ...balancesUpdate, -// }; -// } -// } -// } -// } -// } -// }; - -// const invoiceForUsageImmediately = async ({ -// db, -// intervalToInvoiceItems, -// customer, -// org, -// logger, -// curCusProduct, -// attachParams, -// newSubs, -// }: { -// db: DrizzleCli; -// intervalToInvoiceItems: any; -// customer: any; -// org: any; -// logger: any; -// curCusProduct: FullCusProduct; -// attachParams: AttachParams; -// newSubs: Stripe.Subscription[]; -// }) => { -// // 1. Create invoice -// const stripeCli = createStripeCli({ -// org: org, -// env: customer.env, -// }); -// const product = curCusProduct.product; - -// let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; -// if (invoiceItems.length === 0) { -// return; -// } - -// let invoice: Stripe.Invoice; -// let newInvoice = false; - -// if (attachParams.invoiceOnly && newSubs.length > 0) { -// invoice = await stripeCli.invoices.retrieve( -// newSubs[0].latest_invoice as string, -// ); - -// if (invoice.status !== "draft") { -// newInvoice = true; -// invoice = await stripeCli.invoices.create({ -// customer: customer.processor.id, -// auto_advance: true, -// }); -// } -// } else { -// newInvoice = true; - -// invoice = await stripeCli.invoices.create({ -// customer: customer.processor.id, -// auto_advance: true, -// }); -// } - -// let autumnInvoiceItems: InvoiceItem[] = []; - -// for (const item of invoiceItems) { -// // const amount = getPriceForOverage(item.price, item.overage); -// const { amount, description } = item; -// let config = item.price.config! as UsagePriceConfig; -// // let stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!); -// let stripeProdId = config.stripe_product_id; -// if (!stripeProdId) { -// try { -// let stripePrice = await stripeCli.prices.retrieve( -// config.stripe_price_id!, -// ); -// stripeProdId = stripePrice.product as string; -// } catch (error) {} -// } - -// if (!stripeProdId) { -// stripeProdId = product.processor?.id; -// } - -// logger.info( -// `🌟🌟🌟 (Bill remaining) created invoice item: ${description} -- ${amount}`, -// ); - -// let invoiceItem = { -// customer: customer.processor.id, -// invoice: invoice.id, -// currency: org.default_currency, -// description, -// price_data: { -// product: stripeProdId!, -// unit_amount: Math.round(amount * 100), -// currency: org.default_currency, -// }, -// period: { -// start: item.periodStart, -// end: item.periodEnd, -// }, -// }; - -// let stripeInvoiceItem = await stripeCli.invoiceItems.create(invoiceItem); - -// autumnInvoiceItems.push({ -// price_id: item.price.id!, -// internal_feature_id: item.feature.internal_id || null, -// description: description, -// period_start: item.periodStart * 1000, -// period_end: item.periodEnd * 1000, -// stripe_id: stripeInvoiceItem.id, -// }); - -// await CusEntService.update({ -// db, -// id: item.relatedCusEnt!.id, -// updates: { -// balance: 0, -// }, -// }); -// let index = curCusProduct.customer_entitlements.findIndex( -// (ce) => ce.id === item.relatedCusEnt!.id, -// ); - -// curCusProduct.customer_entitlements[index] = { -// ...curCusProduct.customer_entitlements[index], -// balance: 0, -// }; -// } - -// if (newInvoice) { -// await stripeCli.invoices.finalizeInvoice(invoice.id); - -// const { paid, error } = await payForInvoice({ -// stripeCli, -// paymentMethod: null, -// invoiceId: invoice.id, -// logger, -// }); - -// if (!paid) { -// logger.warn("Failed to pay invoice for remaining usages", { -// stripeInvoice: newInvoice, -// paymentError: error, -// }); -// } -// } - -// await insertInvoiceFromAttach({ -// db, -// attachParams, -// invoiceId: invoice.id, -// logger, -// }); -// }; - -// const getRemainingUsagesPreview = async ({ -// intervalToInvoiceItems, -// curCusProduct, -// }: { -// intervalToInvoiceItems: any; -// curCusProduct: FullCusProduct; -// }) => { -// let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[]; -// if (invoiceItems.length === 0) { -// return; -// } - -// let items = []; -// for (const item of invoiceItems) { -// const amount = getPriceForOverage(item.price, item.overage); -// const description = `${curCusProduct.product.name} - ${ -// item.feature.name -// } x ${Math.round(item.usage)}`; - -// items.push({ -// amount, -// description, -// }); -// } - -// return items; -// }; - -// export const billForRemainingUsages = async ({ -// db, -// logger, -// attachParams, -// curCusProduct, -// newSubs, -// shouldPreview = false, -// billImmediately = false, -// }: { -// db: DrizzleCli; -// logger: any; -// attachParams: AttachParams; -// curCusProduct: FullCusProduct; -// newSubs: Stripe.Subscription[]; -// shouldPreview?: boolean; -// billImmediately?: boolean; -// }) => { -// const { customer_prices, customer_entitlements } = curCusProduct; -// const { customer, org } = attachParams; - -// const intervalToSub: any = {}; - -// for (const sub of newSubs) { -// const interval = subToAutumnInterval(sub); -// if (interval) { -// intervalToSub[interval] = sub; -// } -// } - -// const intervalToInvoiceItems: any = {}; -// const stripeCli = createStripeCli({ -// org: org, -// env: customer.env, -// }); - -// for (const cp of customer_prices) { -// const config = cp.price.config! as UsagePriceConfig; -// const relatedCusEnt = getRelatedCusEnt({ -// cusPrice: cp, -// cusEnts: customer_entitlements, -// }); -// const billingType = getBillingType(config); - -// if (billingType !== BillingType.UsageInArrear) continue; - -// const { usage, overage, description, amount } = getCusPriceUsage({ -// cusPrice: cp, -// cusProduct: curCusProduct, -// logger, -// }); - -// if (overage <= 0) continue; // no overage, no need to bill... - -// let interval = config.interval as BillingInterval; -// if (!intervalToInvoiceItems[interval]) { -// intervalToInvoiceItems[interval] = []; -// } - -// let sub = intervalToSub[interval]; - -// const stripeNow = await getStripeNow({ -// stripeCli, -// stripeSub: sub, -// }); - -// intervalToInvoiceItems[interval].push({ -// overage, -// usage, -// description, -// amount, - -// feature: relatedCusEnt?.entitlement.feature, -// price: cp.price, -// relatedCusEnt, -// periodStart: sub?.current_period_start, -// periodEnd: stripeNow, -// }); -// } - -// if (shouldPreview) { -// return getRemainingUsagesPreview({ -// intervalToInvoiceItems, -// curCusProduct, -// }); -// } - -// if (billImmediately) { -// await invoiceForUsageImmediately({ -// db, -// intervalToInvoiceItems, -// customer, -// org, -// logger, -// curCusProduct, -// attachParams, -// newSubs, -// }); -// } else { -// await addUsageToNextInvoice({ -// db, -// intervalToInvoiceItems, -// intervalToSub, -// customer, -// org, -// logger, -// attachParams, -// }); -// } -// }; diff --git a/server/src/internal/customers/cusUtils/createNewCustomer.ts b/server/src/internal/customers/cusUtils/createNewCustomer.ts index 471c3ef83..537a17620 100644 --- a/server/src/internal/customers/cusUtils/createNewCustomer.ts +++ b/server/src/internal/customers/cusUtils/createNewCustomer.ts @@ -41,7 +41,7 @@ export const createNewCustomer = async ({ const { db, org, env, logger } = req; logger.info( - `Creating customer: ${customer.email || customer.id}, org: ${org.slug}`, + `Creating customer: ${customer.email || customer.id}, org: ${org.slug}` ); const defaultProds = await ProductService.listDefault({ @@ -149,7 +149,10 @@ export const createNewCustomer = async ({ }), nextResetAt, anchorToUnix: org.config.anchor_start_of_month - ? getNextStartOfMonthUnix(BillingInterval.Month) + ? getNextStartOfMonthUnix({ + interval: BillingInterval.Month, + intervalCount: 1, + }) : undefined, scenario: AttachScenario.New, logger, diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts index 35c8274ea..3d974736b 100644 --- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts +++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts @@ -87,11 +87,14 @@ export const featuresToObject = ({ next_reset_at: getEarliestNextResetAt(relatedEnts), interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple", + interval_count: + relatedEnts.length == 1 ? relatedEnts[0].interval_count : null, overage_allowed: relatedEnts.some((e) => e.overage_allowed), breakdown: !unlimited && relatedEnts.length > 1 ? relatedEnts.map((e) => ({ interval: e.interval!, + interval_count: e.interval_count, balance: e.balance, usage: e.usage, included_usage: e.included_usage, diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts index 4d327e8ca..ca30d90d8 100644 --- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts +++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts @@ -257,6 +257,7 @@ export const getCusBalances = async ({ isBoolean || unlimited ? undefined : cusEnt.next_reset_at; data[key].allowance = isBoolean || unlimited ? undefined : 0; data[key].usage_limit = isBoolean || unlimited ? undefined : 0; + data[key].interval_count = ent.interval_count || 1; } } } diff --git a/server/src/internal/customers/handlers/handleUpdateBalances.ts b/server/src/internal/customers/handlers/handleUpdateBalances.ts index 23ec8dfdc..bef4248e4 100644 --- a/server/src/internal/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/customers/handlers/handleUpdateBalances.ts @@ -143,18 +143,21 @@ export const handleUpdateBalances = async (req: any, res: any) => { delete properties.balance; for (const cusEnt of cusEnts) { - if (cusEnt.internal_feature_id !== feature!.internal_id!) { - continue; - } + let cusEntIntCount = cusEnt.entitlement.interval_count || 1; + let deductionIntCount = balance.interval_count || 1; - let intervalCount = cusEnt.entitlement.interval_count || 1; - let intervalCountMatch = - intervalCount > 1 ? balance.interval_count === intervalCount : true; + let intCountMatch = notNullish(balance.interval_count) + ? cusEntIntCount === deductionIntCount + : true; + + let intMatch = notNullish(balance.interval) + ? balance.interval === cusEnt.entitlement.interval + : true; if ( - notNullish(balance.interval) && - balance.interval !== cusEnt.entitlement.interval && - intervalCountMatch + cusEnt.internal_feature_id !== feature!.internal_id! || + !intMatch || + !intCountMatch ) { continue; } @@ -230,9 +233,22 @@ export const handleUpdateBalances = async (req: any, res: any) => { } for (const cusEnt of cusEnts) { + let cusEntIntCount = cusEnt.entitlement.interval_count || 1; + let deductionIntCount = featureDeduction.intervalCount || 1; + + let intCountMatch = notNullish(featureDeduction.intervalCount) + ? cusEntIntCount === deductionIntCount + : true; + + let intMatch = notNullish(featureDeduction.interval) + ? featureDeduction.interval === cusEnt.entitlement.interval + : true; + if ( cusEnt.internal_feature_id !== - featureDeduction.feature!.internal_id! + featureDeduction.feature!.internal_id! || + !intMatch || + !intCountMatch ) { continue; } diff --git a/server/src/internal/products/prices/billingIntervalUtils.ts b/server/src/internal/products/prices/billingIntervalUtils.ts index 030de798b..115412d9b 100644 --- a/server/src/internal/products/prices/billingIntervalUtils.ts +++ b/server/src/internal/products/prices/billingIntervalUtils.ts @@ -3,6 +3,7 @@ import { addMinutes, addMonths, addSeconds, + addWeeks, addYears, differenceInSeconds, getDate, @@ -16,6 +17,7 @@ import { setSeconds, startOfMonth, subMonths, + subWeeks, subYears, } from "date-fns"; import { UTCDate } from "@date-fns/utc"; @@ -33,6 +35,9 @@ export const subtractBillingIntervalUnix = ({ const date = new UTCDate(unixTimestamp); let subtractedDate = date; switch (interval) { + case BillingInterval.Week: + subtractedDate = subWeeks(date, 1 * intervalCount); + break; case BillingInterval.Month: subtractedDate = subMonths(date, 1 * intervalCount); break; @@ -63,6 +68,9 @@ export const addBillingIntervalUnix = ({ const date = new UTCDate(unixTimestamp); let addedDate = date; switch (interval) { + case BillingInterval.Week: + addedDate = addWeeks(date, 1 * intervalCount); + break; case BillingInterval.Month: addedDate = addMonths(date, intervalCount); break; diff --git a/server/src/internal/products/prices/priceUtils.ts b/server/src/internal/products/prices/priceUtils.ts index 38c932c12..d0b902b04 100644 --- a/server/src/internal/products/prices/priceUtils.ts +++ b/server/src/internal/products/prices/priceUtils.ts @@ -131,6 +131,14 @@ export const getBillingInterval = (prices: Price[]) => { throw error; } + // console.log( + // "pricesCopy", + // pricesCopy.map((p) => ({ + // interval: p.config!.interval, + // intervalCount: p.config!.interval_count, + // })) + // ); + if (pricesCopy.length == 0) { throw new RecaseError({ message: "No prices found, can't get billing interval", @@ -140,10 +148,8 @@ export const getBillingInterval = (prices: Price[]) => { } return { - interval: pricesCopy[pricesCopy.length - 1].config! - .interval as BillingInterval, - intervalCount: - pricesCopy[pricesCopy.length - 1].config!.interval_count || 1, + interval: pricesCopy[0].config!.interval as BillingInterval, + intervalCount: pricesCopy[0].config!.interval_count || 1, }; // return pricesCopy[pricesCopy.length - 1].config!.interval as BillingInterval; }; diff --git a/server/src/internal/products/prices/priceUtils/convertPrice.ts b/server/src/internal/products/prices/priceUtils/convertPrice.ts index 617c49a0f..e40104b9d 100644 --- a/server/src/internal/products/prices/priceUtils/convertPrice.ts +++ b/server/src/internal/products/prices/priceUtils/convertPrice.ts @@ -33,8 +33,12 @@ export const toIntervalKey = ({ } else if (interval == BillingInterval.SemiAnnual) { let finalCount = (intervalCount ?? 1) * 6; return `${BillingInterval.Month}-${finalCount}`; + } + + if (interval == BillingInterval.Week) { + return `${BillingInterval.Week}-${intervalCount}`; } else if (interval == BillingInterval.Year) { - return BillingInterval.Year; + return `${BillingInterval.Year}-${intervalCount}`; } return `${interval}-${intervalCount}`; }; diff --git a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts index bb74b82b8..bd42e19b7 100644 --- a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts +++ b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts @@ -36,6 +36,7 @@ const intervalToValue = ( ) => { const intervalToBaseVal: Record = { [BillingInterval.OneOff]: 0, + [BillingInterval.Week]: 0.25, [BillingInterval.Month]: 1, [BillingInterval.Quarter]: 3, [BillingInterval.SemiAnnual]: 6, diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index e6d03166b..cdd1f04b3 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -125,8 +125,8 @@ export const isProductUpgrade = ({ return true; } - let billingInterval1 = getBillingInterval(prices1); - let billingInterval2 = getBillingInterval(prices2); + let billingInterval1 = getBillingInterval(prices1); // pro quarter + let billingInterval2 = getBillingInterval(prices2); // premium // 2. Get total price for each product const getTotalPrice = (prices: Price[]) => { @@ -142,6 +142,7 @@ export const isProductUpgrade = ({ }; // 3. Compare prices + if ( intervalsSame({ intervalA: billingInterval1, diff --git a/server/src/utils/importUtils/addProductFromSubs.ts b/server/src/utils/importUtils/addProductFromSubs.ts index 56e58fa03..fb38378f9 100644 --- a/server/src/utils/importUtils/addProductFromSubs.ts +++ b/server/src/utils/importUtils/addProductFromSubs.ts @@ -51,9 +51,7 @@ export const addProductFromSubs = async ({ (cp) => !cp.product.is_add_on && cp.product_id == autumnProduct.id && - (notNullish(entity) - ? cp.internal_entity_id == entity!.internal_id - : true), + (notNullish(entity) ? cp.internal_entity_id == entity!.internal_id : true) ); if (mainCusProduct && !force) { @@ -66,7 +64,7 @@ export const addProductFromSubs = async ({ autumnCus.id || autumnCus.email } already has non-free free product: ${ mainCusProduct.product.name - }, skipping...`, + }, skipping...` ); return mainCusProduct; } @@ -112,7 +110,7 @@ export const addProductFromSubs = async ({ anchorToUnix: anchorToUnix || stripeSubs[0].current_period_end * 1000, subscriptionStatus: stripeToAutumnSubStatus( - stripeSubs[0].status, + stripeSubs[0].status ) as CusProductStatus, canceledAt: stripeSubs[0].canceled_at @@ -124,7 +122,7 @@ export const addProductFromSubs = async ({ }); logger.info( - `Added product ${autumnProduct.name} to customer ${autumnCus.name}`, + `Added product ${autumnProduct.name} to customer ${autumnCus.name}` ); // Create sub @@ -146,7 +144,7 @@ export const addProductFromSubs = async ({ sub: constructSub({ stripeId: sub.id, usageFeatures: - subInterval == BillingInterval.Month ? usageFeatures : [], + subInterval.interval == BillingInterval.Month ? usageFeatures : [], orgId: org.id, env, currentPeriodStart: sub.current_period_start, diff --git a/server/src/utils/scriptUtils/constructItem.ts b/server/src/utils/scriptUtils/constructItem.ts index 0a7373221..e02b06f10 100644 --- a/server/src/utils/scriptUtils/constructItem.ts +++ b/server/src/utils/scriptUtils/constructItem.ts @@ -61,7 +61,7 @@ export const constructPrepaidItem = ({ }, rolloverConfig, usageLimit, - intervalCount = 2, + intervalCount = 1, }: { featureId: string; price?: number; diff --git a/server/tests/advanced/customInterval/customInterval1 copy.ts b/server/tests/advanced/customInterval/customInterval1 copy.ts deleted file mode 100644 index 6d2a4e673..000000000 --- a/server/tests/advanced/customInterval/customInterval1 copy.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { APIVersion, AppEnv, Organization } from "@autumn/shared"; -import chalk from "chalk"; -import Stripe from "stripe"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { setupBefore } from "tests/before.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; -import { - constructArrearItem, - constructArrearProratedItem, -} from "@/utils/scriptUtils/constructItem.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; - -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { timeout } from "@/utils/genUtils.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; - -const testCase = "upgrade6"; - -export let pro = constructProduct({ - items: [ - constructArrearItem({ featureId: TestFeature.Words }), - constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 20, - }), - ], - type: "pro", -}); - -export let premium = constructProduct({ - items: [ - constructArrearItem({ featureId: TestFeature.Words }), - constructArrearProratedItem({ - featureId: TestFeature.Users, - pricePerUnit: 30, - }), - ], - type: "premium", -}); - -describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => { - let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); - let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, - customerId, - db, - org, - env, - attachPm: "success", - }); - - addPrefixToProducts({ - products: [pro, premium], - prefix: testCase, - }); - - await createProducts({ - autumn, - products: [pro, premium], - db, - orgId: org.id, - env, - }); - - testClockId = testClockId1!; - }); - - it("should attach pro product", async function () { - await runAttachTest({ - autumn, - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); - - let usage = 100012; - it("should upgrade to premium product and fail", async function () { - await autumn.track({ - customer_id: customerId, - feature_id: TestFeature.Words, - value: usage, - }); - await timeout(4000); - - let cus = await CusService.get({ - db, - orgId: org.id, - idOrInternalId: customerId, - env, - }); - - await attachFailedPaymentMethod({ stripeCli, customer: cus! }); - await timeout(2000); - - await expectAutumnError({ - func: async () => { - await runAttachTest({ - autumn, - customerId, - product: premium, - stripeCli, - db, - org, - env, - }); - }, - errMessage: "Failed to update subscription. Your card was declined.", - }); - - await timeout(4000); - let customer = await autumn.customers.get(customerId); - - expectProductAttached({ - customer, - product: pro, - }); - - expectFeaturesCorrect({ - customer, - product: pro, - usage: [ - { - featureId: TestFeature.Words, - value: usage, - }, - ], - }); - - await expectSubItemsCorrect({ - customerId, - product: pro, - stripeCli, - db, - org, - env, - }); - }); -}); diff --git a/server/tests/advanced/customInterval/customInterval1.ts b/server/tests/advanced/customInterval/customInterval1.ts index 360d9e161..8cf7c3f01 100644 --- a/server/tests/advanced/customInterval/customInterval1.ts +++ b/server/tests/advanced/customInterval/customInterval1.ts @@ -1,27 +1,16 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { APIVersion, AppEnv, Organization } from "@autumn/shared"; -import chalk from "chalk"; -import Stripe from "stripe"; + import { DrizzleCli } from "@/db/initDrizzle.js"; import { setupBefore } from "tests/before.js"; import { createProducts } from "tests/utils/productUtils.js"; - -import { - constructArrearItem, - constructArrearProratedItem, - constructFeatureItem, -} from "@/utils/scriptUtils/constructItem.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; - -import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { timeout } from "@/utils/genUtils.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js"; -import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addHours, addMonths } from "date-fns"; @@ -35,13 +24,9 @@ export let pro = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Words, - intervalCount: 200, + intervalCount: 2, + includedUsage: 500, }), - // constructArrearItem({ featureId: TestFeature.Words }), - // constructArrearProratedItem({ - // featureId: TestFeature.Users, - // pricePerUnit: 20, - // }), ], intervalCount: 2, type: "pro", @@ -51,7 +36,7 @@ export let premium = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Words, - intervalCount: 500, + intervalCount: 2, }), // constructArrearItem({ featureId: TestFeature.Words }), // constructArrearProratedItem({ @@ -63,7 +48,7 @@ export let premium = constructProduct({ type: "premium", }); -describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => { +describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interval count`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -153,5 +138,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => expect(invoices.length).to.equal(3); expect(invoices[0].product_ids).to.include(premium.id); expect(invoices[0].total).to.equal(getBasePrice({ product: premium })); + + const wordsFeature = customer2.features[TestFeature.Words]; + // @ts-ignore + expect(wordsFeature.interval_count).to.equal(2); }); }); diff --git a/server/tests/advanced/customInterval/customInteral2.ts b/server/tests/advanced/customInterval/customInterval2.ts similarity index 100% rename from server/tests/advanced/customInterval/customInteral2.ts rename to server/tests/advanced/customInterval/customInterval2.ts diff --git a/server/tests/advanced/customInterval/customInterval3.ts b/server/tests/advanced/customInterval/customInterval3.ts index ea89242f8..b7f4c2148 100644 --- a/server/tests/advanced/customInterval/customInterval3.ts +++ b/server/tests/advanced/customInterval/customInterval3.ts @@ -54,6 +54,7 @@ const prepaidWordsItem = constructPrepaidItem({ price: 10, billingUnits: 1, includedUsage: 0, + intervalCount: 2, }); export const addOn = constructRawProduct({ @@ -62,7 +63,7 @@ export const addOn = constructRawProduct({ isAddOn: true, }); -describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => { +describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on merged product`)}`, () => { let customerId = testCase; let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; @@ -115,7 +116,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p }); }); - it("should upgrade to premium product and have correct invoice next cycle", async function () { + it("should upgrade to attached add on and have correct invoice next cycle", async function () { const curUnix = await advanceTestClock({ stripeCli, testClockId, @@ -150,22 +151,21 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear p }); let expectedPrice = wordsBillingUnits * prepaidWordsItem.price!; - expect(invoices[0].product_ids).to.include(addOn.id); - expect(invoices[0].total).to.approximately( - calculateProrationAmount({ - amount: expectedPrice, - periodStart: curUnix!, - periodEnd: addMonths(curUnix!, 1).getTime(), - now: curUnix!, - }), - 0.1 - ); + const proratedPrice = calculateProrationAmount({ + amount: expectedPrice, + periodStart: new Date().getTime(), + periodEnd: addMonths(new Date(), 2).getTime(), + now: curUnix!, + }); - const expectedAddonEnd = addMonths(curUnix, 1); + expect(invoices[0].product_ids).to.include(addOn.id); + expect(invoices[0].total).to.approximately(proratedPrice, 0.1); + + const expectedAddonEnd = addMonths(new Date(), 2); const approximate = 1000 * 60 * 60 * 24; // +- 1 day const addOnProduct = customer.products.find((p) => p.id === addOn.id); - expect(addOnProduct?.current_period_end).to.be.closeTo( + expect(addOnProduct?.current_period_end).to.be.approximately( expectedAddonEnd.getTime(), approximate ); diff --git a/server/tests/advanced/customInterval/customInterval4.ts b/server/tests/advanced/customInterval/customInterval4.ts new file mode 100644 index 000000000..056ee8893 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval4.ts @@ -0,0 +1,150 @@ +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import chalk from "chalk"; +import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; + +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { addMonths } from "date-fns"; +import { expect } from "chai"; +import { + expectDowngradeCorrect, + expectNextCycleCorrect, +} from "tests/utils/expectUtils/expectScheduleUtils.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; + +const testCase = "customInterval4"; + +export let pro = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 500, + }), + ], + intervalCount: 2, + type: "pro", +}); + +export let premium = constructProduct({ + id: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 500, + }), + ], + intervalCount: 2, + type: "premium", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom intervals`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro, premium], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach premium product", async function () { + await runAttachTest({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + }); + }); + + it("should have correct next cycle at on checkout", async function () { + const checkout = await autumn.checkout({ + customer_id: customerId, + product_id: pro.id, + }); + + let expectedNextCycle = addMonths(new Date(), 2); + expect(checkout.next_cycle?.starts_at).to.be.approximately( + expectedNextCycle.getTime(), + 1000 * 60 * 60 * 24 + ); + + expect(checkout.total).to.equal(0); + }); + + let preview: any; + it("should downgrade to pro", async function () { + const { preview: preview_ } = await expectDowngradeCorrect({ + autumn, + customerId, + curProduct: premium, + newProduct: pro, + stripeCli, + db, + org, + env, + }); + + preview = preview_; + }); + + it("should have pro attached on next cycle", async function () { + await expectNextCycleCorrect({ + preview: preview!, + autumn, + stripeCli, + customerId, + testClockId, + product: pro, + db, + org, + env, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).to.equal(2); + expect(invoices[0].total).to.equal(getBasePrice({ product: pro })); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval5.ts b/server/tests/advanced/customInterval/customInterval5.ts new file mode 100644 index 000000000..5fab46f4f --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval5.ts @@ -0,0 +1,163 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, FullCustomer, Organization } from "@autumn/shared"; + +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { expect } from "chai"; +import { Customer } from "autumn-js"; +import { timeout } from "@/utils/genUtils.js"; + +const testCase = "customInterval5"; + +const includedUsage = 500; +const monthlyWords = constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage, +}); + +const biMonthlyWords = constructFeatureItem({ + featureId: TestFeature.Words, + intervalCount: 2, + includedUsage, +}); + +export let pro = constructProduct({ + items: [monthlyWords, biMonthlyWords], + intervalCount: 2, + type: "pro", +}); + +const getBreakdown = ({ + customer, + intervalCount, +}: { + customer: Customer; + intervalCount: number; +}) => { + const wordsFeature = customer.features[TestFeature.Words]; + // @ts-ignore + return wordsFeature.breakdown?.find( + (b: any) => b.interval_count == intervalCount + ); +}; + +describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + it("should attach pro product", async function () { + await runAttachTest({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + }); + + const customer = await autumn.customers.get(customerId); + const wordsFeature = customer.features[TestFeature.Words]; + // @ts-ignore + expect(wordsFeature.interval_count).to.equal(null); + expect(wordsFeature.breakdown?.length).to.equal(2); + + expect( + wordsFeature.breakdown?.some( + (b: any) => b.interval_count == 1 && b.interval == "month" + ) + ).to.equal(true); + expect( + wordsFeature.breakdown?.some( + (b: any) => b.interval_count == 2 && b.interval == "month" + ) + ).to.equal(true); + }); + + const trackVal = 300; + it("should have correct breakdown after usage", async function () { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: trackVal, + }); + + await timeout(3000); + + const customer = await autumn.customers.get(customerId); + + // Should deduct + const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 }); + const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 }); + + expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal); + expect(biMonthlyBreakdown?.balance).to.equal(includedUsage); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: trackVal, + }); + + await timeout(3000); + + const customer2 = await autumn.customers.get(customerId); + const monthlyBreakdown2 = getBreakdown({ + customer: customer2, + intervalCount: 1, + }); + const biMonthlyBreakdown2 = getBreakdown({ + customer: customer2, + intervalCount: 2, + }); + + expect(monthlyBreakdown2?.balance).to.equal(0); + expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100); + }); +}); diff --git a/server/tests/advanced/customInterval/customInterval6.ts b/server/tests/advanced/customInterval/customInterval6.ts new file mode 100644 index 000000000..5f0899982 --- /dev/null +++ b/server/tests/advanced/customInterval/customInterval6.ts @@ -0,0 +1,165 @@ +import chalk from "chalk"; +import Stripe from "stripe"; + +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { APIVersion, AppEnv, FullCustomer, Organization } from "@autumn/shared"; + +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { setupBefore } from "tests/before.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { addPrefixToProducts, runAttachTest } from "tests/attach/utils.js"; +import { expect } from "chai"; +import { Customer } from "autumn-js"; +import { timeout } from "@/utils/genUtils.js"; + +const testCase = "customInterval6"; + +// Update balances! + +// const includedUsage = 500; +// const monthlyWords = constructFeatureItem({ +// featureId: TestFeature.Words, +// includedUsage, +// }); + +// const biMonthlyWords = constructFeatureItem({ +// featureId: TestFeature.Words, +// intervalCount: 2, +// includedUsage, +// }); + +// export let pro = constructProduct({ +// items: [monthlyWords, biMonthlyWords], +// intervalCount: 2, +// type: "pro", +// }); + +// const getBreakdown = ({ +// customer, +// intervalCount, +// }: { +// customer: Customer; +// intervalCount: number; +// }) => { +// const wordsFeature = customer.features[TestFeature.Words]; +// // @ts-ignore +// return wordsFeature.breakdown?.find( +// (b: any) => b.interval_count == intervalCount +// ); +// }; + +// describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => { +// let customerId = testCase; +// let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); +// let testClockId: string; +// let db: DrizzleCli, org: Organization, env: AppEnv; +// let stripeCli: Stripe; + +// before(async function () { +// await setupBefore(this); +// const { autumnJs } = this; +// db = this.db; +// org = this.org; +// env = this.env; + +// stripeCli = this.stripeCli; + +// const { testClockId: testClockId1 } = await initCustomer({ +// autumn: autumnJs, +// customerId, +// db, +// org, +// env, +// attachPm: "success", +// }); + +// addPrefixToProducts({ +// products: [pro], +// prefix: testCase, +// }); + +// await createProducts({ +// autumn, +// products: [pro], +// db, +// orgId: org.id, +// env, +// }); + +// testClockId = testClockId1!; +// }); + +// it("should attach pro product", async function () { +// await runAttachTest({ +// autumn, +// customerId, +// product: pro, +// stripeCli, +// db, +// org, +// env, +// }); + +// const customer = await autumn.customers.get(customerId); +// const wordsFeature = customer.features[TestFeature.Words]; +// // @ts-ignore +// expect(wordsFeature.interval_count).to.equal(null); +// expect(wordsFeature.breakdown?.length).to.equal(2); + +// expect( +// wordsFeature.breakdown?.some( +// (b: any) => b.interval_count == 1 && b.interval == "month" +// ) +// ).to.equal(true); +// expect( +// wordsFeature.breakdown?.some( +// (b: any) => b.interval_count == 2 && b.interval == "month" +// ) +// ).to.equal(true); +// }); + +// const trackVal = 300; +// it("should have correct breakdown after usage", async function () { +// await autumn.track({ +// customer_id: customerId, +// feature_id: TestFeature.Words, +// value: trackVal, +// }); + +// await timeout(3000); + +// const customer = await autumn.customers.get(customerId); + +// // Should deduct +// const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 }); +// const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 }); + +// expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal); +// expect(biMonthlyBreakdown?.balance).to.equal(includedUsage); + +// await autumn.track({ +// customer_id: customerId, +// feature_id: TestFeature.Words, +// value: trackVal, +// }); + +// await timeout(3000); + +// const customer2 = await autumn.customers.get(customerId); +// const monthlyBreakdown2 = getBreakdown({ +// customer: customer2, +// intervalCount: 1, +// }); +// const biMonthlyBreakdown2 = getBreakdown({ +// customer: customer2, +// intervalCount: 2, +// }); + +// expect(monthlyBreakdown2?.balance).to.equal(0); +// expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100); +// }); +// }); diff --git a/server/tests/utils/productUtils.ts b/server/tests/utils/productUtils.ts index 2a81a92a1..eab69f507 100644 --- a/server/tests/utils/productUtils.ts +++ b/server/tests/utils/productUtils.ts @@ -36,7 +36,7 @@ export const createProduct = async ({ internalId: prod.internal_id, orgId, env, - }), + }) ); } @@ -82,7 +82,7 @@ export const createProducts = async ({ const batchCreate = []; for (const product of products) { batchCreate.push( - createProduct({ db, orgId, env, autumn, product, prefix }), + createProduct({ db, orgId, env, autumn, product, prefix }) ); } @@ -113,12 +113,12 @@ export const createReward = async ({ idOrInternalId: productId!, }); - let usagePrices = fullProduct.prices.filter((price) => - isUsagePrice({ price }), + let usagePrices = fullProduct?.prices.filter((price) => + isUsagePrice({ price }) ); if (onlyUsage) { - reward.discount_config!.price_ids = usagePrices.map((price) => price.id); + reward.discount_config!.price_ids = usagePrices?.map((price) => price.id); } try { diff --git a/shared/models/productModels/priceModels/priceEnums.ts b/shared/models/productModels/priceModels/priceEnums.ts index fbc36b181..e9b7b1a2d 100644 --- a/shared/models/productModels/priceModels/priceEnums.ts +++ b/shared/models/productModels/priceModels/priceEnums.ts @@ -1,5 +1,6 @@ export enum BillingInterval { OneOff = "one_off", + Week = "week", Month = "month", Quarter = "quarter", SemiAnnual = "semi_annual", diff --git a/shared/utils/intervalUtils.ts b/shared/utils/intervalUtils.ts index fb88ce05f..c029082e5 100644 --- a/shared/utils/intervalUtils.ts +++ b/shared/utils/intervalUtils.ts @@ -8,6 +8,7 @@ export const intervalToValue = ( ) => { const intervalToBaseVal: Record = { [BillingInterval.OneOff]: 0, + [BillingInterval.Week]: 0.25, [BillingInterval.Month]: 1, [BillingInterval.Quarter]: 3, [BillingInterval.SemiAnnual]: 6, From ae7dcd8a757a1be2e2a87dff2a3c03744db82e3d Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 10 Aug 2025 08:27:27 +0100 Subject: [PATCH 32/37] fix: schedule function and getExistingUsage --- server/shell/g3.sh | 15 +++++- server/shell/g4.sh | 6 +-- .../supabase/subscribeToOrgUpdates.ts | 49 +++++++------------ .../scheduleFlow/handleScheduleFunction.ts | 6 ++- .../cusEnts/cusEntUtils/getExistingUsage.ts | 4 +- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/server/shell/g3.sh b/server/shell/g3.sh index 8fb17499d..60aa554da 100755 --- a/server/shell/g3.sh +++ b/server/shell/g3.sh @@ -15,4 +15,17 @@ $MOCHA_CMD 'tests/contUse/update/*.ts' $MOCHA_CMD 'tests/contUse/track/*.ts' -$MOCHA_CMD 'tests/contUse/roles/*.ts' \ No newline at end of file +$MOCHA_CMD 'tests/contUse/roles/*.ts' + +# G4 +$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ + 'tests/advanced/coupons/*.ts' \ + 'tests/attach/updateQuantity/*.ts' \ + 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' + +$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ + 'tests/advanced/usageLimit/*.ts' + +$MOCHA_CMD 'tests/advanced/usage/*.ts' \ No newline at end of file diff --git a/server/shell/g4.sh b/server/shell/g4.sh index 75aa8212b..03aa2ece4 100755 --- a/server/shell/g4.sh +++ b/server/shell/g4.sh @@ -15,8 +15,8 @@ $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ 'tests/advanced/rollovers/*.ts' \ 'tests/advanced/customInterval/*.ts' -# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ -# 'tests/advanced/usageLimit/*.ts' +$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ + 'tests/advanced/usageLimit/*.ts' -# $MOCHA_CMD 'tests/advanced/usage/*.ts' +$MOCHA_CMD 'tests/advanced/usage/*.ts' \ No newline at end of file diff --git a/server/src/external/supabase/subscribeToOrgUpdates.ts b/server/src/external/supabase/subscribeToOrgUpdates.ts index 8170e4264..f0fa1524d 100644 --- a/server/src/external/supabase/subscribeToOrgUpdates.ts +++ b/server/src/external/supabase/subscribeToOrgUpdates.ts @@ -1,32 +1,21 @@ import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; -import { createSupabaseClient } from "../supabaseUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { safeSb } from "./safeSb.js"; +import { DrizzleCli, client } from "@/db/initDrizzle.js"; -export const subscribeToOrgUpdates = safeSb({ - fn: ({ db }: { db: DrizzleCli }) => { - try { - const sb = createSupabaseClient(); - sb.channel("table-db-changes") - .on( - "postgres_changes", - { - event: "UPDATE", - schema: "public", - table: "organizations", - }, - async (payload) => { - try { - await clearOrgCache({ db, orgId: payload.new.id }); - } catch (error) { - console.warn("Error clearing org cache:", error); - } - }, - ) - .subscribe(); - } catch (error) { - console.warn("Error subscribing to org updates:", error); - } - }, - action: "subscribe to org updates", -}); +export const subscribeToOrgUpdates = async ({ db }: { db: DrizzleCli }) => { + try { + await client.listen("org_updates", async (payload) => { + try { + const data = JSON.parse(payload); + if (data.table === "organizations" && data.operation === "UPDATE") { + await clearOrgCache({ db, orgId: data.new.id }); + } + } catch (error) { + console.warn("Error processing org update notification:", error); + } + }); + + console.log("Successfully subscribed to organization updates via PostgreSQL LISTEN/NOTIFY"); + } catch (error) { + console.warn("Error subscribing to org updates:", error); + } +}; diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts index c87046602..407f5e080 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFunction.ts @@ -29,7 +29,7 @@ export const handleScheduleFunction = async ({ }) => { const logger = req.logtail; const product = attachParams.products[0]; - const { stripeCli } = attachParams; + const { stripeCli, customer: fullCus } = attachParams; const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({ attachParams, @@ -55,7 +55,9 @@ export const handleScheduleFunction = async ({ // 3. Get schedules for current cus products logger.info(`3. Getting schedules for current cus products`); let schedules = await cusProductsToSchedules({ - cusProducts: [curMainProduct, curScheduledProduct], + // cusProducts: [curMainProduct, curScheduledProduct], + // cusProducts: [curMainProduct, curScheduledProduct], + cusProducts: fullCus.customer_products, stripeCli, }); diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts index 70aa06475..f80dd28e2 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts @@ -81,7 +81,7 @@ export const getExistingUsages = ({ let feature = features.find( (f) => f.internal_id === entity.internal_feature_id ); - let key = `${feature?.id}-${EntInterval.Lifetime}`; + let key = `${feature?.id}-${EntInterval.Lifetime}-1`; if (!usages[key]) { usages[key] = { @@ -212,7 +212,7 @@ export const addExistingUsagesToCusEnts = ({ for (const cusEnt of fullCusEnts) { let ent = cusEnt.entitlement; - let cusEntKey = `${ent.feature_id}-${ent.interval}`; + let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`; let fromEntities = existingUsages[key].fromEntities; if (cusEntKey !== key) { From 9cde29b079e8d1dc18bf2803cf0cd6e4dea296de Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 10 Aug 2025 09:04:01 +0100 Subject: [PATCH 33/37] fix: attach modal and update sub diff int --- server/shell/g1.sh | 5 ++- server/src/index.ts | 3 +- .../attach/attachUtils/getAttachConfig.ts | 24 +++++++++++++- .../internal/invoices/invoiceFormatUtils.ts | 11 +++++-- .../product-items/compareItemUtils.ts | 31 +++++++++++++------ .../productItemUtils/handleNewProductItems.ts | 2 -- .../getProductItemDisplay.ts | 6 ++-- 7 files changed, 62 insertions(+), 20 deletions(-) diff --git a/server/shell/g1.sh b/server/shell/g1.sh index 157db3eb5..e0df33004 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -3,7 +3,10 @@ # Source shared configuration source "$(dirname "$0")/config.sh" -# MOCHA_PARALLEL=true $MOCHA_SETUP \ +# If contains setup then run $MOCHA_SETUP +if [[ "$1" == *"setup"* ]]; then + MOCHA_PARALLEL=true $MOCHA_SETUP +fi $MOCHA_CMD \ 'tests/attach/basic/*.ts' \ diff --git a/server/src/index.ts b/server/src/index.ts index 7df9d0490..a1fb4937f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -30,6 +30,7 @@ import { ClickHouseManager } from "./external/clickhouse/ClickHouseManager.js"; const tracer = trace.getTracer("express"); checkEnvVars(); +// subscribeToOrgUpdates({ db }); const init = async () => { const app = express(); @@ -84,8 +85,6 @@ const init = async () => { await CacheManager.getInstance(); await ClickHouseManager.getInstance(); - subscribeToOrgUpdates({ db }); - app.use(async (req: any, res: any, next: any) => { req.env = req.env = req.headers["app_env"] || AppEnv.Sandbox; req.db = db; diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts index 811f23f7d..910228d92 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts @@ -1,6 +1,11 @@ import { AttachParams } from "../../cusProducts/AttachParams.js"; import { AttachFlags } from "../models/AttachFlags.js"; -import { AttachConfig, AttachBranch, intervalsSame } from "@autumn/shared"; +import { + AttachConfig, + AttachBranch, + intervalsSame, + intervalToValue, +} from "@autumn/shared"; import { AttachBody } from "@autumn/shared"; import { isFreeProduct } from "@/internal/products/productUtils.js"; import { nullish } from "@/utils/genUtils.js"; @@ -27,6 +32,23 @@ export const intervalsAreSame = ({ let newProduct = attachParamsToProduct({ attachParams }); let curPrices = cusProductToPrices({ cusProduct: curCusProduct! }); + const curIntervals = new Set( + curPrices.map((p) => + intervalToValue(p.config.interval, p.config.interval_count) + ) + ); + + const newIntervals = new Set( + newProduct.prices.map((p) => + intervalToValue(p.config.interval, p.config.interval_count) + ) + ); + + return ( + curIntervals.size === newIntervals.size && + [...curIntervals].every((interval) => newIntervals.has(interval)) + ); + for (const price of curPrices) { let hasSimilarInterval = newProduct.prices.some((p) => { return intervalsSame({ diff --git a/server/src/internal/invoices/invoiceFormatUtils.ts b/server/src/internal/invoices/invoiceFormatUtils.ts index e190f7b71..72f320095 100644 --- a/server/src/internal/invoices/invoiceFormatUtils.ts +++ b/server/src/internal/invoices/invoiceFormatUtils.ts @@ -11,6 +11,7 @@ import { getFeatureNameWithCapital, Organization, Price, + ProductItemInterval, UsagePriceConfig, } from "@autumn/shared"; import { @@ -25,6 +26,7 @@ import { import { getFeatureQuantity } from "../customers/cusProducts/cusProductUtils.js"; import { formatAmount } from "@/utils/formatUtils.js"; import { getIntervalString } from "../products/productUtils/productResponseUtils/getProductItemDisplay.js"; +import { billingToItemInterval } from "../products/product-items/itemIntervalUtils.js"; const getSingularAndPlural = (feature: Feature) => { const singular = getFeatureName({ @@ -73,11 +75,16 @@ export const formatFixedPrice = ({ const config = price.config as FixedPriceConfig; const amount = formatAmount({ org, amount: config.amount }); - // const intervalStr = getIntervalString({}); + const intervalStr = getIntervalString({ + interval: billingToItemInterval(config.interval) as ProductItemInterval, + intervalCount: config.interval_count || 1, + prefix: "", + }); + if (config.interval == BillingInterval.OneOff) { return `${amount}`; } else { - return `${amount} / ${config.interval}`; + return `${amount} / ${intervalStr}`; } }; diff --git a/server/src/internal/products/product-items/compareItemUtils.ts b/server/src/internal/products/product-items/compareItemUtils.ts index c2c8129f3..67dfc8cab 100644 --- a/server/src/internal/products/product-items/compareItemUtils.ts +++ b/server/src/internal/products/product-items/compareItemUtils.ts @@ -33,7 +33,10 @@ export const findSimilarItem = ({ if (isPriceItem(item)) { return items.find((i) => { return ( - isPriceItem(i) && i.price === item.price && i.interval === item.interval + isPriceItem(i) && + i.price === item.price && + i.interval === item.interval && + (i.interval_count || 1) == (item.interval_count || 1) ); }); } @@ -68,16 +71,17 @@ export const featureItemsAreSame = ({ item2: FeatureItem; }) => { // Compare config objects (including rollover) - const configsAreSame = JSON.stringify(item1.config) === JSON.stringify(item2.config); - - const same = ( + const configsAreSame = + JSON.stringify(item1.config) === JSON.stringify(item2.config); + + const same = item1.feature_id === item2.feature_id && item1.included_usage == item2.included_usage && item1.interval == item2.interval && + (item1.interval_count || 1) == (item2.interval_count || 1) && item1.entity_feature_id == item2.entity_feature_id && item1.reset_usage_when_enabled == item2.reset_usage_when_enabled && - configsAreSame - ); + configsAreSame; return same; }; @@ -89,7 +93,10 @@ export const priceItemsAreSame = ({ item1: PriceItem; item2: PriceItem; }) => { - const same = item1.price === item2.price && item1.interval == item2.interval; + const same = + item1.price === item2.price && + item1.interval == item2.interval && + (item1.interval_count || 1) == (item2.interval_count || 1); if (!same) { console.log(`Price items different: ${item1.price}`); @@ -134,6 +141,10 @@ export const featurePriceItemsAreSame = ({ condition: item1.interval == item2.interval, message: `Interval different: ${item1.interval} != ${item2.interval}`, }, + interval_count: { + condition: (item1.interval_count || 1) == (item2.interval_count || 1), + message: `Interval count different: ${item1.interval_count} != ${item2.interval_count}`, + }, usage_model: { condition: item1.usage_model === item2.usage_model, message: `Usage model different: ${item1.usage_model} != ${item2.usage_model}`, @@ -194,8 +205,6 @@ export const itemsAreSame = ({ let same = false; let pricesChanged = false; - - if (isFeatureItem(item1)) { if (!isFeatureItem(item2)) { return { @@ -246,7 +255,9 @@ export const itemsAreSame = ({ item1: PriceItemSchema.parse(item1), item2: PriceItemSchema.parse(item2), }); - pricesChanged = false; + if (!same) { + pricesChanged = true; + } } return { diff --git a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts index 0452b61f1..271829697 100644 --- a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts +++ b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts @@ -253,8 +253,6 @@ export const handleNewProductItems = async ({ } } - console.log("updatedEnts", updatedEnts); - if (newFeatures.length > 0 && saveToDb) { await FeatureService.insert({ db, diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts index ddb9717bf..8da066d96 100644 --- a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts +++ b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts @@ -21,15 +21,17 @@ import { export const getIntervalString = ({ interval, intervalCount, + prefix = "per ", }: { interval: ProductItemInterval; intervalCount?: number | null; + prefix?: string; }) => { if (!interval) return ""; if (intervalCount == 1) { - return `per ${interval}`; + return `${prefix}${interval}`; } - return `per ${intervalCount} ${interval}s`; + return `${prefix}${intervalCount} ${interval}s`; }; export const formatTiers = ({ From 5e7941f9af0cabd254611dc41470123e0501905e Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 10 Aug 2025 09:19:42 +0100 Subject: [PATCH 34/37] fix: try catch for tests --- server/run.sh | 2 +- server/shell/g1.sh | 6 ++-- server/tests/attach/basic/basic2.ts | 1 + server/tests/attach/basic/basic4.ts | 10 +++--- server/tests/before.ts | 52 ++++++++++++++--------------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/server/run.sh b/server/run.sh index 1caeaa1a6..308986454 100755 --- a/server/run.sh +++ b/server/run.sh @@ -8,7 +8,7 @@ filename=$1 if [[ $filename == *"shell"* ]]; then - $filename + $filename "${@:2}" elif [[ $filename == *"/tests/"* ]]; then # Extract everything after "/tests/" path_after_tests=$(echo "$filename" | sed 's/.*\/tests\///') diff --git a/server/shell/g1.sh b/server/shell/g1.sh index e0df33004..87e738c10 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -4,9 +4,9 @@ source "$(dirname "$0")/config.sh" # If contains setup then run $MOCHA_SETUP -if [[ "$1" == *"setup"* ]]; then - MOCHA_PARALLEL=true $MOCHA_SETUP -fi +# MOCHA_PARALLEL=true $MOCHA_SETUP +# if [[ "$2" == *"setup"* ]]; then +# fi $MOCHA_CMD \ 'tests/attach/basic/*.ts' \ diff --git a/server/tests/attach/basic/basic2.ts b/server/tests/attach/basic/basic2.ts index 9ae9f6068..7bcfd4412 100644 --- a/server/tests/attach/basic/basic2.ts +++ b/server/tests/attach/basic/basic2.ts @@ -23,6 +23,7 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => { before(async function () { await setupBefore(this); + db = this.db; org = this.org; env = this.env; diff --git a/server/tests/attach/basic/basic4.ts b/server/tests/attach/basic/basic4.ts index f2b1cd9ce..508892cf3 100644 --- a/server/tests/attach/basic/basic4.ts +++ b/server/tests/attach/basic/basic4.ts @@ -88,16 +88,16 @@ describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => (e: any) => e.feature_id === features.metered1.id && e.interval == - products.monthlyAddOnMetered1.entitlements.metered1.interval, + products.monthlyAddOnMetered1.entitlements.metered1.interval ); expect(monthlyMetered1Balance!.balance).to.equal( - proMetered1! + monthlyQuantity, + proMetered1! + monthlyQuantity ); expect(cusRes.add_ons).to.have.lengthOf(1); const monthlyAddOnId = cusRes.add_ons.find( - (a: any) => a.id === products.monthlyAddOnMetered1.id, + (a: any) => a.id === products.monthlyAddOnMetered1.id ); expect(monthlyAddOnId).to.exist; @@ -108,14 +108,14 @@ describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => const res: any = await AutumnCli.entitled(customerId, features.metered1.id); const metered1Balance = res!.balances.find( - (b: any) => b.feature_id === features.metered1.id, + (b: any) => b.feature_id === features.metered1.id ); const proMetered1Amt = products.pro.entitlements.metered1.allowance; const monthlyAddOnMetered1Amt = monthlyQuantity; expect(metered1Balance!.balance).to.equal( - proMetered1Amt! + monthlyAddOnMetered1Amt, + proMetered1Amt! + monthlyAddOnMetered1Amt ); }); }); diff --git a/server/tests/before.ts b/server/tests/before.ts index 931f206ec..3562c10b5 100644 --- a/server/tests/before.ts +++ b/server/tests/before.ts @@ -19,34 +19,34 @@ const hyperbrowser = new Hyperbrowser({ }); export const setupBefore = async (instance: any) => { - const { db, client } = initDrizzle(); + try { + const { db, client } = initDrizzle(); - const org = await OrgService.getBySlug({ db, slug: ORG_SLUG }); - if (!org) { - throw new Error("Org not found"); + const org = await OrgService.getBySlug({ db, slug: ORG_SLUG }); + if (!org) { + throw new Error("Org not found"); + } + const env = DEFAULT_ENV; + const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!; + const autumn = new AutumnInt({ apiKey: autumnSecretKey }); + + const autumnJs = new AutumnJS({ + secretKey: autumnSecretKey, + url: "http://localhost:8080/v1", + }); + + const stripeCli = createStripeCli({ org, env }); + instance.org = org; + instance.env = env; + instance.autumn = autumn; + instance.stripeCli = stripeCli; + instance.autumnJs = autumnJs; + instance.db = db; + instance.client = client; + } catch (error) { + console.log("Error setting up before", error); + throw error; } - const env = DEFAULT_ENV; - const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!; - const autumn = new AutumnInt({ apiKey: autumnSecretKey }); - - // console.log("instance.browserSession", instance.browserSession); - // if (!instance.browserSession) { - // instance.browserSession = await hyperbrowser.sessions.create(); - // } - - const autumnJs = new AutumnJS({ - secretKey: autumnSecretKey, - url: "http://localhost:8080/v1", - }); - - const stripeCli = createStripeCli({ org, env }); - instance.org = org; - instance.env = env; - instance.autumn = autumn; - instance.stripeCli = stripeCli; - instance.autumnJs = autumnJs; - instance.db = db; - instance.client = client; // Return a cleanup function after(async () => { From 447960d8b84344ded9e030481903ce8bf18f1376 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 10 Aug 2025 09:26:32 +0100 Subject: [PATCH 35/37] reduced number of workers --- server/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index a1fb4937f..769a8147a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -179,7 +179,7 @@ if (process.env.NODE_ENV === "development") { console.log(`Master ${process.pid} is running`); console.log("Number of CPUs", numCPUs); - let numWorkers = 10; + let numWorkers = 7; for (let i = 0; i < numWorkers; i++) { cluster.fork(); From 42a5cdf35785324e1a1c3f503532620451babe06 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 10 Aug 2025 22:18:51 +0100 Subject: [PATCH 36/37] fix: billing cycle one off formatting --- .gitignore | 3 ++- server/tests/attach/basic/basic2.ts | 2 +- server/tests/attach/basic/basic9.ts | 4 ++-- shared/package.json | 3 ++- vite/src/utils/formatUtils/formatTextUtils.ts | 12 +++++++----- .../components/feature-price/SelectBillingCycle.tsx | 1 + 6 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 87930686c..a502a6d08 100644 --- a/.gitignore +++ b/.gitignore @@ -101,4 +101,5 @@ supabase/ # To load supabase # 1. Start supabase locally -# 2. Load files \ No newline at end of file +# 2. Load files +migration.sh \ No newline at end of file diff --git a/server/tests/attach/basic/basic2.ts b/server/tests/attach/basic/basic2.ts index 7bcfd4412..6e99007f8 100644 --- a/server/tests/attach/basic/basic2.ts +++ b/server/tests/attach/basic/basic2.ts @@ -45,7 +45,7 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => { }); await completeCheckoutForm(checkout_url); - await timeout(10000); + await timeout(12000); }); it("should have correct product & entitlements", async function () { diff --git a/server/tests/attach/basic/basic9.ts b/server/tests/attach/basic/basic9.ts index 826c9553a..d9e24ea1a 100644 --- a/server/tests/attach/basic/basic9.ts +++ b/server/tests/attach/basic/basic9.ts @@ -9,7 +9,7 @@ import { compareMainProduct } from "tests/utils/compare.js"; const testCase = "basic9"; describe(`${chalk.yellowBright( - "basic9: attach monthly with one time prepaid, and quantity = 0", + "basic9: attach monthly with one time prepaid, and quantity = 0" )}`, () => { let customerId = testCase; @@ -40,7 +40,7 @@ describe(`${chalk.yellowBright( }); await completeCheckoutForm(res.checkout_url); - await timeout(10000); + await timeout(12000); }); it("should have correct main product and entitlements", async function () { diff --git a/shared/package.json b/shared/package.json index 41cac0732..05a3b7a04 100644 --- a/shared/package.json +++ b/shared/package.json @@ -19,7 +19,8 @@ "dev:bun": "bun ./index.ts --outdir dist --target bun --external zod --watch", "db:push": "bun db:generate && bun db:migrate", "db:generate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit generate --config drizzle.config.ts", - "db:migrate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit migrate --config drizzle.config.ts" + "db:migrate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit migrate --config drizzle.config.ts", + "db:studio": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit studio --config drizzle.config.ts" }, "dependencies": { "date-fns": "^4.1.0", diff --git a/vite/src/utils/formatUtils/formatTextUtils.ts b/vite/src/utils/formatUtils/formatTextUtils.ts index 476a36155..99d121179 100644 --- a/vite/src/utils/formatUtils/formatTextUtils.ts +++ b/vite/src/utils/formatUtils/formatTextUtils.ts @@ -46,20 +46,22 @@ export const formatIntervalText = ({ interval, intervalCount, billingInterval, + isBillingInterval = false, }: { interval?: EntInterval; - billingInterval?: BillingInterval; intervalCount?: number; + isBillingInterval?: boolean; }) => { const finalInterval = interval ?? billingInterval; if (finalInterval == null) { return ""; } - if ( - finalInterval === EntInterval.Lifetime || - finalInterval === BillingInterval.OneOff - ) { + + if (finalInterval === BillingInterval.OneOff) { + return "one off"; + } + if (finalInterval === EntInterval.Lifetime) { return "no reset"; } if (intervalCount && intervalCount > 1) { diff --git a/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx index d49ce4425..171bae1cb 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx @@ -64,6 +64,7 @@ export const SelectCycle = () => { {formatIntervalText({ billingInterval: interval, intervalCount: item.interval_count, + isBillingInterval: true, })} ))} From 45d9ce1de917949c81e4d45565ff8ccbc40b69ad Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 11 Aug 2025 11:56:10 +0100 Subject: [PATCH 37/37] fix: detect product variant, credit system UI --- server/shell/g1.sh | 6 +- server/src/index.ts | 2 - server/src/internal/features/featureRouter.ts | 2 +- .../features/handlers/handleUpdateFeature.ts | 22 ++-- .../products/handlers/handleVersionProduct.ts | 17 ++- server/src/internal/products/productUtils.ts | 2 + .../productUtils/detectProductVariant.ts | 12 +- .../getProductResponse.ts | 5 +- server/src/queue/workersInit.ts | 10 +- server/tests/utils/setup.ts | 124 ++++++++++-------- vite/src/components/autumn/pricing-table.tsx | 2 - vite/src/views/credits/CreditSystemConfig.tsx | 22 ++-- vite/src/views/credits/UpdateCreditSystem.tsx | 3 +- .../edit-product/EditProductDetails.tsx | 1 - 14 files changed, 134 insertions(+), 96 deletions(-) diff --git a/server/shell/g1.sh b/server/shell/g1.sh index 87e738c10..a47b9d773 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -4,9 +4,9 @@ source "$(dirname "$0")/config.sh" # If contains setup then run $MOCHA_SETUP -# MOCHA_PARALLEL=true $MOCHA_SETUP -# if [[ "$2" == *"setup"* ]]; then -# fi +if [[ "$2" == *"setup"* ]]; then +MOCHA_PARALLEL=true $MOCHA_SETUP +fi $MOCHA_CMD \ 'tests/attach/basic/*.ts' \ diff --git a/server/src/index.ts b/server/src/index.ts index 769a8147a..9e6adcfe8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -46,8 +46,6 @@ const init = async () => { "https://staging.useautumn.com", "https://*.useautumn.com", "https://localhost:8080", - "https://app.aidvize.com", - "http://staging.aidvize.com", "https://www.alphalog.ai", "https://*.alphalog.ai", process.env.CLIENT_URL || "", diff --git a/server/src/internal/features/featureRouter.ts b/server/src/internal/features/featureRouter.ts index becaa4823..4b8ef9fb9 100644 --- a/server/src/internal/features/featureRouter.ts +++ b/server/src/internal/features/featureRouter.ts @@ -122,7 +122,7 @@ featureRouter.post("/:feature_id", async (req: any, res: any) => } if (apiFeature.credit_schema) { - newConfig.credit_schema = apiFeature.credit_schema.map((credit) => ({ + newConfig.schema = apiFeature.credit_schema.map((credit) => ({ metered_feature_id: credit.metered_feature_id, credit_amount: credit.credit_cost, })); diff --git a/server/src/internal/features/handlers/handleUpdateFeature.ts b/server/src/internal/features/handlers/handleUpdateFeature.ts index 3a6e65a52..d4a51fdff 100644 --- a/server/src/internal/features/handlers/handleUpdateFeature.ts +++ b/server/src/internal/features/handlers/handleUpdateFeature.ts @@ -350,6 +350,17 @@ export const handleUpdateFeature = async (req: any, res: any) => } } + const newConfig = + data.config !== undefined + ? feature.type == FeatureType.CreditSystem + ? validateCreditSystem(data.config) + : feature.type == FeatureType.Metered + ? validateMeteredConfig(data.config) + : data.config + : feature.config; + + console.log(`feature: ${feature.id}, new config:`, newConfig); + let updatedFeature = await FeatureService.update({ db: req.db, id: featureId, @@ -359,15 +370,10 @@ export const handleUpdateFeature = async (req: any, res: any) => id: data.id !== undefined ? data.id : feature.id, name: data.name !== undefined ? data.name : feature.name, type: data.type !== undefined ? data.type : feature.type, - archived: data.archived !== undefined ? data.archived : feature.archived, + archived: + data.archived !== undefined ? data.archived : feature.archived, - config: data.config !== undefined - ? (feature.type == FeatureType.CreditSystem - ? validateCreditSystem(data.config) - : feature.type == FeatureType.Metered - ? validateMeteredConfig(data.config) - : data.config) - : feature.config, + config: newConfig, }, }); diff --git a/server/src/internal/products/handlers/handleVersionProduct.ts b/server/src/internal/products/handlers/handleVersionProduct.ts index 6942ce124..1747eded1 100644 --- a/server/src/internal/products/handlers/handleVersionProduct.ts +++ b/server/src/internal/products/handlers/handleVersionProduct.ts @@ -59,6 +59,7 @@ export const handleVersionProductV2 = async ({ orgId: org.id, env: latestProduct.env as AppEnv, processor: latestProduct.processor, + baseVariantId: latestProduct.base_variant_id, }); // Validate product items... @@ -114,12 +115,16 @@ export const handleVersionProductV2 = async ({ }); } - await addTaskToQueue({ - jobName: JobName.DetectBaseVariant, - payload: { - curProduct: newProduct, - }, - }); + // await addTaskToQueue({ + // jobName: JobName.DetectBaseVariant, + // payload: { + // curProduct: { + // ...newProduct, + // // prices: customPrices, + // // entitlements: getEntsWithFeature({ ents: customEnts, features }), + // }, + // }, + // }); await initProductInStripe({ db, diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index cdd1f04b3..851ef41b4 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -76,11 +76,13 @@ export const constructProduct = ({ orgId, env, processor, + baseVariantId, }: { productData: CreateProduct; orgId: string; env: AppEnv; processor?: any; + baseVariantId?: string | null; }) => { let newProduct: Product = { ...productData, diff --git a/server/src/internal/products/productUtils/detectProductVariant.ts b/server/src/internal/products/productUtils/detectProductVariant.ts index 637bdb1e2..62f9b7788 100644 --- a/server/src/internal/products/productUtils/detectProductVariant.ts +++ b/server/src/internal/products/productUtils/detectProductVariant.ts @@ -32,6 +32,7 @@ export const detectBaseVariant = async ({ curProduct: FullProduct; logger: Logger; }) => { + logger.info(`Detecting base variant for ${curProduct.id}`); if (!process.env.ANTHROPIC_API_KEY) return; let existingProducts = (await ProductService.listFull({ @@ -48,10 +49,12 @@ export const detectBaseVariant = async ({ // 1. Return null if add on if (curProduct.is_add_on) return null; - // // 2. Return null if only one off or monthly price + // 2. Return null if only one off or monthly price const oneOffOrMonthly = [BillingInterval.OneOff, BillingInterval.Month]; - if (intervals.every((i: BillingInterval) => oneOffOrMonthly.includes(i))) + if (intervals.every((i: BillingInterval) => oneOffOrMonthly.includes(i))) { + logger.info(`Is one off or monthly, skipping`); return null; + } const filteredExistingProducts = existingProducts.filter( (p) => @@ -65,7 +68,10 @@ export const detectBaseVariant = async ({ p.group == curProduct.group ); - if (filteredExistingProducts.length == 0) return null; + if (filteredExistingProducts.length == 0) { + logger.info(`No base product to search for`); + return null; + } const variables = ` diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts index 99500f23c..2ac56e5f7 100644 --- a/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts +++ b/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts @@ -133,7 +133,10 @@ export const getProductProperties = ({ product: FullProduct; freeTrial?: FreeTrialResponse | null; }) => { - const largestInterval = getLargestInterval({ prices: product.prices }); + const largestInterval = getLargestInterval({ + prices: product.prices, + excludeOneOff: true, + }); let hasFreeTrial = notNullish(freeTrial) && freeTrial?.trial_available !== false; diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index fbbe03249..9cd3dfc27 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -15,6 +15,7 @@ import { runActionHandlerTask } from "@/internal/analytics/runActionHandlerTask. import { logger } from "@/external/logtail/logtailUtils.js"; import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js"; import { Logger } from "pino"; +import { generateId } from "@/utils/genUtils.js"; const NUM_WORKERS = 10; @@ -44,6 +45,7 @@ const initWorker = ({ worker: { task: job.name, data: job.data, + jobId: generateId("job"), workerId: id, }, }, @@ -87,11 +89,13 @@ const initWorker = ({ }); return; } - } catch (error) { + } catch (error: any) { logtail.error(`Failed to process bullmq job: ${job.name}`, { jobName: job.name, - jobData: job.data, - error, + error: { + message: error.message, + stack: error.stack, + }, }); } diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index ca1b7fb6d..d625ec433 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -294,67 +294,79 @@ export const setupOrg = async ({ // 2. Create products let insertProducts = []; - for (let i = 0; i < Object.values(products).length; i++) { - const product = Object.values(products)[i]; - const insertProduct = async () => { - await autumn.products.create({ - id: product.id, - name: product.name, - group: product.group, - is_add_on: product.is_add_on, - is_default: product.is_default, - }); + const productValues = Object.values(products); + const batchSize = 5; - const prices = product.prices.map((p: any) => ({ - ...p, - config: { - ...p.config, - internal_feature_id: newFeatures!.find( - (f) => f.id === (p.config as any)?.feature_id - )?.internal_id, - }, - })); + for ( + let batchStart = 0; + batchStart < productValues.length; + batchStart += batchSize + ) { + const batch = productValues.slice(batchStart, batchStart + batchSize); + const batchPromises = []; - const entitlements = Object.values(product.entitlements).map( - (ent: any) => ({ - ...ent, - internal_feature_id: newFeatures!.find((f) => f.id === ent.feature_id) - ?.internal_id, - }) - ); - - const entWithFeatures = entitlements.map((ent) => ({ - ...ent, - feature: newFeatures!.find((f) => f.id === ent.feature_id), - })); - - let items = mapToProductItems({ - prices, - entitlements: entWithFeatures, - allowFeatureMatch: true, - features: newFeatures!, - }); - - try { - await axiosInstance.post(`/v1/products/${product.id}`, { - // prices: prices, - // entitlements: entitlements, - items, - free_trial: product.free_trial, + for (const product of batch) { + const insertProduct = async () => { + await autumn.products.create({ + id: product.id, + name: product.name, + group: product.group, + is_add_on: product.is_add_on, + is_default: product.is_default, }); - } catch (error) { - console.log("Product:", product.name); - console.error("Error creating product prices / ents"); - console.log("Items", items); - } - return; - }; - insertProducts.push(insertProduct()); + const prices = product.prices.map((p: any) => ({ + ...p, + config: { + ...p.config, + internal_feature_id: newFeatures!.find( + (f) => f.id === (p.config as any)?.feature_id + )?.internal_id, + }, + })); - // if (i > 1) { - // break; - // } + const entitlements = Object.values(product.entitlements).map( + (ent: any) => ({ + ...ent, + internal_feature_id: newFeatures!.find( + (f) => f.id === ent.feature_id + )?.internal_id, + }) + ); + + const entWithFeatures = entitlements.map((ent) => ({ + ...ent, + feature: newFeatures!.find((f) => f.id === ent.feature_id), + })); + + let items = mapToProductItems({ + prices, + entitlements: entWithFeatures, + allowFeatureMatch: true, + features: newFeatures!, + }); + + try { + await axiosInstance.post(`/v1/products/${product.id}`, { + // prices: prices, + // entitlements: entitlements, + items, + free_trial: product.free_trial, + }); + } catch (error) { + console.log("Product:", product.name); + console.error("Error creating product prices / ents"); + console.log("Items", items); + } + return; + }; + + batchPromises.push(insertProduct()); + } + + // Wait for the current batch to complete before proceeding to the next + await Promise.all(batchPromises); + insertProducts.push(...batchPromises); } await Promise.all(insertProducts); diff --git a/vite/src/components/autumn/pricing-table.tsx b/vite/src/components/autumn/pricing-table.tsx index d1b7226c1..a74d3115f 100644 --- a/vite/src/components/autumn/pricing-table.tsx +++ b/vite/src/components/autumn/pricing-table.tsx @@ -83,8 +83,6 @@ export default function PricingTable({ openInNewTab: true, successUrl: `${window.location.origin}`, }); - - console.log("Result:", result); } else if (product.display?.button_url) { window.open(product.display?.button_url, "_blank"); } diff --git a/vite/src/views/credits/CreditSystemConfig.tsx b/vite/src/views/credits/CreditSystemConfig.tsx index 2b82597be..99969e781 100644 --- a/vite/src/views/credits/CreditSystemConfig.tsx +++ b/vite/src/views/credits/CreditSystemConfig.tsx @@ -127,17 +127,17 @@ function CreditSystemConfig({
{creditSystemConfig.schema.map((item: any, index: number) => ( - -
-
+
+
+
-
+
- +
))}
diff --git a/vite/src/views/credits/UpdateCreditSystem.tsx b/vite/src/views/credits/UpdateCreditSystem.tsx index ee2f9ebed..eba2895b6 100644 --- a/vite/src/views/credits/UpdateCreditSystem.tsx +++ b/vite/src/views/credits/UpdateCreditSystem.tsx @@ -18,6 +18,7 @@ import { CustomDialogContent, CustomDialogFooter, } from "@/components/general/modal-components/DialogContentWrapper"; +import { getBackendErr } from "@/utils/genUtils"; function UpdateCreditSystem({ open, @@ -53,7 +54,7 @@ function UpdateCreditSystem({ await mutate(); setOpen(false); } catch (error) { - toast.error("Failed to update credit system"); + toast.error(getBackendErr(error, "Failed to update credit system")); } setUpdateLoading(false); }; diff --git a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx index 2e79ddb13..ccf517f56 100644 --- a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx +++ b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx @@ -29,7 +29,6 @@ export const EditProductDetails = () => { }); useEffect(() => { - console.log("product:", product); if (product.id) { setDetails({ name: product.name,