added svix on org created and deleting webhooks on org deleted
This commit is contained in:
@@ -93,6 +93,8 @@ export default function CustomerProductView({
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [useInvoice, setUseInvoice] = useState(false);
|
||||
const initialProductRef = useRef<FrontendProduct | null>(null);
|
||||
const [selectedEntitlementAllowance, setSelectedEntitlementAllowance] =
|
||||
useState<"unlimited" | number>(0);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -298,6 +300,8 @@ export default function CustomerProductView({
|
||||
env,
|
||||
product,
|
||||
setProduct,
|
||||
selectedEntitlementAllowance,
|
||||
setSelectedEntitlementAllowance,
|
||||
// prices: product.prices,
|
||||
// entitlements: product.entitlements,
|
||||
org,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nodemon --no-deprecation --exec tsx src/index.ts --ignore scripts",
|
||||
"dev": "nodemon --no-deprecation --exec tsx src/index.ts --ignore scripts --ignore tests",
|
||||
"start": "tsx src/index.ts",
|
||||
"queue:dev": "tsx watch src/queue.ts",
|
||||
"build": "tsc -b",
|
||||
@@ -17,7 +17,8 @@
|
||||
"clean": "tsx src/clean.ts",
|
||||
"test-all": "mocha 'tests/**/*.ts'",
|
||||
"test": "mocha 'tests/**/*.ts'",
|
||||
"test-custom": "mocha 'tests/02_usage.ts'"
|
||||
"test-upgrade": "mocha 'tests/06_upgrade.ts' 'tests/07_downgrade.ts'",
|
||||
"test-custom": "mocha 'tests/06_upgrade.ts'"
|
||||
},
|
||||
"mocha": {
|
||||
"node-option": [
|
||||
|
||||
52
server/src/external/autumn/autumnWebhookRouter.ts
vendored
Normal file
52
server/src/external/autumn/autumnWebhookRouter.ts
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
import express from "express";
|
||||
import { Webhook } from "svix";
|
||||
|
||||
export const autumnWebhookRouter = express.Router();
|
||||
|
||||
const verifyAutumnWebhook = async (req: any, res: any) => {
|
||||
const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
|
||||
|
||||
const headers = req.headers;
|
||||
const payload = req.body;
|
||||
|
||||
const svix_id = headers["svix-id"];
|
||||
const svix_timestamp = headers["svix-timestamp"];
|
||||
const svix_signature = headers["svix-signature"];
|
||||
|
||||
if (!svix_id || !svix_timestamp || !svix_signature) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "Error: Missing svix headers",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let evt: any;
|
||||
try {
|
||||
evt = wh.verify(payload, {
|
||||
"svix-id": svix_id as string,
|
||||
"svix-timestamp": svix_timestamp as string,
|
||||
"svix-signature": svix_signature as string,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Error: Could not verify webhook");
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "Error: Could not verify webhook",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return evt;
|
||||
};
|
||||
|
||||
autumnWebhookRouter.post(
|
||||
"",
|
||||
express.raw({ type: "application/json" }),
|
||||
async (req, res) => {
|
||||
console.log("Webhook from autumn");
|
||||
|
||||
const evt = await verifyAutumnWebhook(req, res);
|
||||
console.log(evt);
|
||||
}
|
||||
);
|
||||
24
server/src/external/stripe/stripeCusUtils.ts
vendored
24
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -71,22 +71,26 @@ export const getCusPaymentMethod = async ({
|
||||
stripeId
|
||||
)) as Stripe.Customer;
|
||||
|
||||
const paymentMethod = stripeCustomer.invoice_settings.default_payment_method;
|
||||
let paymentMethodId = stripeCustomer.invoice_settings.default_payment_method;
|
||||
|
||||
if (!paymentMethod) {
|
||||
const paymentMethods = await stripeCli.paymentMethods.list({
|
||||
if (!paymentMethodId) {
|
||||
let res = await stripeCli.paymentMethods.list({
|
||||
customer: stripeId,
|
||||
type: "card",
|
||||
});
|
||||
|
||||
if (paymentMethods.data.length === 0) {
|
||||
// const paymentMethods = res.data.filter((pm) => pm.type === "card" );
|
||||
|
||||
const paymentMethods = res.data;
|
||||
paymentMethods.sort((a, b) => b.created - a.created);
|
||||
|
||||
if (res.data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return paymentMethods.data[0].id;
|
||||
return paymentMethods[0].id;
|
||||
}
|
||||
|
||||
return paymentMethod;
|
||||
return paymentMethodId;
|
||||
};
|
||||
|
||||
// 2. Create a payment method and attach to customer
|
||||
@@ -107,7 +111,7 @@ export const attachPmToCus = async ({
|
||||
}) => {
|
||||
// 1. Create stripe customer if not exists
|
||||
|
||||
let stripeCusId = customer.processor?.stripe_id;
|
||||
let stripeCusId = customer.processor?.id;
|
||||
if (!stripeCusId) {
|
||||
const stripeCustomer = await createStripeCustomer({
|
||||
org,
|
||||
@@ -126,6 +130,10 @@ export const attachPmToCus = async ({
|
||||
})
|
||||
.eq("internal_id", customer.internal_id);
|
||||
stripeCusId = stripeCustomer.id;
|
||||
customer.processor = {
|
||||
id: stripeCustomer.id,
|
||||
type: "stripe",
|
||||
};
|
||||
}
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
85
server/src/external/stripe/stripePriceUtils.ts
vendored
85
server/src/external/stripe/stripePriceUtils.ts
vendored
@@ -2,7 +2,6 @@ import {
|
||||
BillingInterval,
|
||||
BillingType,
|
||||
FixedPriceConfig,
|
||||
PriceOptions,
|
||||
Organization,
|
||||
FullProduct,
|
||||
Price,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
Product,
|
||||
AllowanceType,
|
||||
EntitlementWithFeature,
|
||||
CusProductStatus,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
@@ -20,6 +20,8 @@ import { ErrCode } from "@/errors/errCodes.js";
|
||||
import Stripe from "stripe";
|
||||
import {
|
||||
getBillingType,
|
||||
getCheckoutRelevantPrices,
|
||||
getEntOptions,
|
||||
getPriceAmount,
|
||||
getPriceEntitlement,
|
||||
getPriceOptions,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
import { PriceService } from "@/internal/prices/PriceService.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { AttachParams } from "@/internal/customers/products/AttachParams.js";
|
||||
import { createStripeCli } from "./utils.js";
|
||||
export const billingIntervalToStripe = (interval: BillingInterval) => {
|
||||
switch (interval) {
|
||||
case BillingInterval.Month:
|
||||
@@ -54,20 +57,19 @@ export const billingIntervalToStripe = (interval: BillingInterval) => {
|
||||
}
|
||||
};
|
||||
|
||||
// GET STRIPE LINE / SUB ITEM
|
||||
export const priceToStripeItem = ({
|
||||
price,
|
||||
product,
|
||||
org,
|
||||
options,
|
||||
isCheckout = false,
|
||||
relatedEnt,
|
||||
}: {
|
||||
price: Price;
|
||||
product: FullProduct;
|
||||
org: Organization;
|
||||
options: FeatureOptions | undefined | null;
|
||||
isCheckout: boolean;
|
||||
relatedEnt: EntitlementWithFeature | undefined;
|
||||
}) => {
|
||||
// TODO: Implement this
|
||||
const billingType = price.billing_type;
|
||||
@@ -108,7 +110,12 @@ export const priceToStripeItem = ({
|
||||
};
|
||||
} else if (billingType == BillingType.UsageInAdvance) {
|
||||
const config = price.config as UsagePriceConfig;
|
||||
const quantity = options?.quantity || 1;
|
||||
// const quantity = options?.quantity || 1;
|
||||
|
||||
if (options?.quantity === 0 && isCheckout) {
|
||||
console.log(`Quantity for ${config.feature_id} is 0`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const adjustableQuantity = isCheckout
|
||||
? {
|
||||
@@ -126,7 +133,7 @@ export const priceToStripeItem = ({
|
||||
|
||||
lineItem = {
|
||||
price: config.stripe_price_id,
|
||||
quantity,
|
||||
quantity: options?.quantity!,
|
||||
adjustable_quantity: adjustableQuantity,
|
||||
};
|
||||
lineItemMeta = {
|
||||
@@ -158,6 +165,74 @@ export const priceToStripeItem = ({
|
||||
};
|
||||
};
|
||||
|
||||
// STRIPE TO SUB ITEMS
|
||||
export const getStripeSubItems = async ({
|
||||
attachParams,
|
||||
isCheckout = false,
|
||||
}: {
|
||||
attachParams: AttachParams;
|
||||
isCheckout?: boolean;
|
||||
}) => {
|
||||
const { product, prices, entitlements, optionsList, org, curCusProduct } =
|
||||
attachParams;
|
||||
const checkoutRelevantPrices = getCheckoutRelevantPrices(prices);
|
||||
|
||||
let subItems: any[] = [];
|
||||
let itemMetas: any[] = [];
|
||||
|
||||
// TODO: Check if non bill now prices can be added to stripe subscription...?
|
||||
|
||||
// // 1. Check current period end...
|
||||
// if (curCusProduct && curCusProduct.processor?.subscription_id) {
|
||||
// const subId = curCusProduct.processor.subscription_id;
|
||||
// const stripeCli = createStripeCli({
|
||||
// org,
|
||||
// env: curCusProduct.customer.env,
|
||||
// });
|
||||
|
||||
// const sub = await stripeCli.subscriptions.retrieve(subId);
|
||||
|
||||
// const prorationConfig: any = {};
|
||||
// if (sub.status !== CusProductStatus.Trialing) {
|
||||
// const curPeriodStart = sub.current_period_start * 1000;
|
||||
// const curPeriodEnd = sub.current_period_end * 1000;
|
||||
|
||||
// prorationConfig.current_period_start = curPeriodStart;
|
||||
// prorationConfig.current_period_end = curPeriodEnd;
|
||||
|
||||
// const curPrices = curCusProduct.customer_prices.map((p) => p.price!);
|
||||
|
||||
// prorationConfig.curPrices = curPrices;
|
||||
// }
|
||||
// }
|
||||
|
||||
for (const price of checkoutRelevantPrices) {
|
||||
const priceEnt = getPriceEntitlement(price, entitlements);
|
||||
const options = getEntOptions(optionsList, priceEnt);
|
||||
|
||||
const stripeItem = priceToStripeItem({
|
||||
price,
|
||||
product,
|
||||
org,
|
||||
options,
|
||||
isCheckout,
|
||||
});
|
||||
|
||||
if (!stripeItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { lineItem, lineItemMeta } = stripeItem;
|
||||
|
||||
subItems.push(lineItem);
|
||||
itemMetas.push(lineItemMeta);
|
||||
}
|
||||
|
||||
console.log("Line items: ", subItems);
|
||||
|
||||
return { items: subItems, itemMetas };
|
||||
};
|
||||
|
||||
export const inAdvanceToStripeTiers = (
|
||||
price: Price,
|
||||
entitlement: Entitlement
|
||||
|
||||
2
server/src/external/stripe/stripeSubUtils.ts
vendored
2
server/src/external/stripe/stripeSubUtils.ts
vendored
@@ -127,7 +127,7 @@ export const updateStripeSubscription = async ({
|
||||
});
|
||||
return subUpdate;
|
||||
} catch (error: any) {
|
||||
console.log("Error updating stripe subscription", error.message);
|
||||
console.log("Error updating stripe subscription.", error.message);
|
||||
|
||||
if (isStripeCardDeclined(error)) {
|
||||
throw new RecaseError({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Stripe } from "stripe";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { CusProductService } from "@/internal/customers/products/CusProductService.js";
|
||||
import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js";
|
||||
import { AppEnv, Organization, UsagePriceConfig } from "@autumn/shared";
|
||||
import { AppEnv, Organization } from "@autumn/shared";
|
||||
import { AttachParams } from "@/internal/customers/products/AttachParams.js";
|
||||
import { createStripeCli } from "../utils.js";
|
||||
import { InvoiceService } from "@/internal/customers/invoices/InvoiceService.js";
|
||||
@@ -121,6 +121,8 @@ export const handleCheckoutSessionCompleted = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upgrade / downgrade
|
||||
|
||||
console.log(" - checkout.completed: creating full customer product");
|
||||
|
||||
await createFullCusProduct({
|
||||
|
||||
39
server/src/external/svix/svixUtils.ts
vendored
Normal file
39
server/src/external/svix/svixUtils.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import { AppEnv } from "@shared/models/genModels.js";
|
||||
import { Svix } from "svix";
|
||||
|
||||
export const createSvixCli = () => {
|
||||
return new Svix(process.env.SVIX_API_KEY as string);
|
||||
};
|
||||
|
||||
export const createSvixApp = async ({
|
||||
name,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
name: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const svix = createSvixCli();
|
||||
const app = await svix.application.create({
|
||||
name,
|
||||
metadata: {
|
||||
org_id: orgId,
|
||||
env,
|
||||
},
|
||||
});
|
||||
return app;
|
||||
};
|
||||
|
||||
export const deleteSvixApp = async ({ appId }: { appId: string }) => {
|
||||
const svix = createSvixCli();
|
||||
await svix.application.delete(appId);
|
||||
};
|
||||
|
||||
export const sendSvixEvent = async (event: any) => {
|
||||
const svix = createSvixCli();
|
||||
await svix.message.create("app_2tKDzBZtEBMQoybfckgdnb3BlJ0", {
|
||||
eventType: "product.attached",
|
||||
payload: event,
|
||||
});
|
||||
};
|
||||
93
server/src/external/webhooks/clerkWebhooks.ts
vendored
93
server/src/external/webhooks/clerkWebhooks.ts
vendored
@@ -24,6 +24,12 @@ import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
|
||||
import { PriceService } from "@/internal/prices/PriceService.js";
|
||||
import { getBillingType } from "@/internal/prices/priceUtils.js";
|
||||
import { createSvixApp, deleteSvixApp } from "../svix/svixUtils.js";
|
||||
import {
|
||||
deleteStripeWebhook,
|
||||
initOrgSvixApps,
|
||||
} from "@/internal/orgs/orgUtils.js";
|
||||
import { createStripeCli } from "../stripe/utils.js";
|
||||
|
||||
const defaultFeatures = [
|
||||
{
|
||||
@@ -222,11 +228,13 @@ export const handleClerkWebhook = async (req: any, res: any) => {
|
||||
break;
|
||||
|
||||
case "organization.deleted":
|
||||
await OrgService.delete({
|
||||
sb: req.sb,
|
||||
orgId: eventData.id,
|
||||
});
|
||||
console.log(`Deleted org ${eventData.id}`);
|
||||
await handleOrgDeleted(req.sb, eventData);
|
||||
break;
|
||||
// await OrgService.delete({
|
||||
// sb: req.sb,
|
||||
// orgId: eventData.id,
|
||||
// });
|
||||
// console.log(`Deleted org ${eventData.id}`);
|
||||
|
||||
default:
|
||||
break;
|
||||
@@ -248,7 +256,17 @@ export const handleClerkWebhook = async (req: any, res: any) => {
|
||||
};
|
||||
|
||||
const handleOrgCreated = async (sb: SupabaseClient, eventData: any) => {
|
||||
console.log(
|
||||
`Handling organization.created: ${eventData.slug} (${eventData.id})`
|
||||
);
|
||||
try {
|
||||
// 1. Create svix webhoooks
|
||||
const { sandboxApp, liveApp } = await initOrgSvixApps({
|
||||
slug: eventData.slug,
|
||||
id: eventData.id,
|
||||
});
|
||||
|
||||
// 2. Insert org
|
||||
await OrgService.insert({
|
||||
sb,
|
||||
org: {
|
||||
@@ -259,6 +277,11 @@ const handleOrgCreated = async (sb: SupabaseClient, eventData: any) => {
|
||||
stripe_config: null,
|
||||
test_pkey: generatePublishableKey(AppEnv.Sandbox),
|
||||
live_pkey: generatePublishableKey(AppEnv.Live),
|
||||
created_at: eventData.created_at,
|
||||
svix_config: {
|
||||
sandbox_app_id: sandboxApp.id,
|
||||
live_app_id: liveApp.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -292,3 +315,63 @@ const handleOrgCreated = async (sb: SupabaseClient, eventData: any) => {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrgDeleted = async (sb: SupabaseClient, eventData: any) => {
|
||||
console.log(
|
||||
`Handling organization.deleted: ${eventData.slug} (${eventData.id})`
|
||||
);
|
||||
|
||||
const org = await OrgService.getFullOrg({
|
||||
sb,
|
||||
orgId: eventData.id,
|
||||
});
|
||||
|
||||
// 1. Delete svix webhooks
|
||||
|
||||
try {
|
||||
console.log("1. Deleting svix webhooks");
|
||||
const batch = [];
|
||||
if (org.svix_config.sandbox_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.sandbox_app_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (org.svix_config.live_app_id) {
|
||||
batch.push(
|
||||
deleteSvixApp({
|
||||
appId: org.svix_config.live_app_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batch);
|
||||
|
||||
// 2. Delete stripe webhooks
|
||||
console.log("2. Deleting stripe webhooks");
|
||||
if (org.stripe_config) {
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
|
||||
await deleteStripeWebhook({
|
||||
org: org,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Delete org
|
||||
console.log("3. Deleting org");
|
||||
await OrgService.delete({
|
||||
sb,
|
||||
orgId: eventData.id,
|
||||
});
|
||||
|
||||
console.log(`Deleted org ${org.slug} (${org.id})`);
|
||||
} catch (error) {
|
||||
console.log("Failed to delete organization", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,14 @@ import express from "express";
|
||||
import bodyParser from "body-parser";
|
||||
import { handleClerkWebhook } from "./clerkWebhooks.js";
|
||||
import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js";
|
||||
import { autumnWebhookRouter } from "../autumn/autumnWebhookRouter.js";
|
||||
|
||||
const webhooksRouter = express.Router();
|
||||
|
||||
webhooksRouter.use("/stripe", stripeWebhookRouter);
|
||||
|
||||
webhooksRouter.use("/autumn", autumnWebhookRouter);
|
||||
|
||||
webhooksRouter.post(
|
||||
"/clerk",
|
||||
bodyParser.raw({ type: "application/json" }),
|
||||
|
||||
@@ -557,15 +557,17 @@ cusRouter.post(
|
||||
// Reactivate current product
|
||||
const curActiveProducts = await CusService.getFullCusProducts({
|
||||
sb: req.sb,
|
||||
internalCustomerId: cusProduct.customer.internal_id,
|
||||
withProduct: true,
|
||||
internalCustomerId: cusProduct.internal_customer_id,
|
||||
inStatuses: [CusProductStatus.Active],
|
||||
productGroup: cusProduct.product.group,
|
||||
withProduct: true,
|
||||
});
|
||||
|
||||
const activeProducts = curActiveProducts.filter(
|
||||
(p: any) => p.product.group == cusProduct.product.group
|
||||
);
|
||||
for (const activeProduct of activeProducts) {
|
||||
for (const activeProduct of curActiveProducts) {
|
||||
console.log(
|
||||
"Reactivating current product:",
|
||||
activeProduct.product.name
|
||||
);
|
||||
await stripeCli.subscriptions.update(
|
||||
activeProduct.processor.subscription_id!,
|
||||
{
|
||||
@@ -596,61 +598,6 @@ cusRouter.post(
|
||||
});
|
||||
}
|
||||
}
|
||||
// console.log(cusProduct);
|
||||
// if (!cusProduct.product.is_add_on) {
|
||||
// if (cusProduct.status == CusProductStatus.Scheduled) {
|
||||
// console.log(
|
||||
// `Cancelling scheduled product ${cusProduct.product.name} for ${customerId}`
|
||||
// );
|
||||
|
||||
// try {
|
||||
// await stripeCli.subscriptionSchedules.cancel(
|
||||
// cusProduct.processor.subscription_schedule_id!
|
||||
// );
|
||||
// } catch (error: any) {
|
||||
// console.log("Failed to cancel scheduled product:", error.message);
|
||||
// }
|
||||
|
||||
// console.log("Updating status to expired");
|
||||
// await CusProductService.deleteFutureProduct({
|
||||
// sb: req.sb,
|
||||
// internalCustomerId: cusProduct.customer.internal_id,
|
||||
// productGroup: cusProduct.product.group,
|
||||
// });
|
||||
|
||||
// // Re activate current product
|
||||
// console.log("Reactivating current product");
|
||||
// const curActiveProducts = await CusService.getFullCusProducts({
|
||||
// sb: req.sb,
|
||||
// internalCustomerId: cusProduct.customer.internal_id,
|
||||
// withProduct: true,
|
||||
// inStatuses: [CusProductStatus.Active],
|
||||
// });
|
||||
|
||||
// const activeProducts = curActiveProducts.filter(
|
||||
// (p: any) => p.product.group == cusProduct.product.group
|
||||
// );
|
||||
// for (const activeProduct of activeProducts) {
|
||||
// await stripeCli.subscriptions.update(
|
||||
// activeProduct.processor.subscription_id!,
|
||||
// {
|
||||
// cancel_at: null,
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// } else if (!cusProduct.processor.subscription_id) {
|
||||
// // Don't need to delete, stripe will do it...
|
||||
// // console.log(
|
||||
// // `Expiring product ${cusProduct.product.name} for ${customerId} (attaching default if exists)`
|
||||
// // );
|
||||
// await expireAndAddDefaultProduct({
|
||||
// sb: req.sb,
|
||||
// org,
|
||||
// env: req.env,
|
||||
// cusProduct,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
if (!cusProduct) {
|
||||
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
} from "@/internal/prices/priceUtils.js";
|
||||
import { PricesInput } from "@autumn/shared";
|
||||
import { getFullCusProductData } from "../../../customers/products/cusProductUtils.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import {
|
||||
isFreeProduct,
|
||||
isProductUpgrade,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import { handleAddFreeProduct } from "@/internal/customers/add-product/handleAddFreeProduct.js";
|
||||
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
@@ -41,6 +44,7 @@ import {
|
||||
createStripePriceIFNotExist,
|
||||
} from "@/external/stripe/stripePriceUtils.js";
|
||||
import { handleInvoiceOnly } from "@/internal/customers/add-product/handleInvoiceOnly.js";
|
||||
import { notNullOrUndefined } from "@/utils/genUtils.js";
|
||||
|
||||
export const attachRouter = Router();
|
||||
|
||||
@@ -88,13 +92,21 @@ export const checkAddProductErrors = async ({
|
||||
// Get options for price
|
||||
let priceEnt = getPriceEntitlement(price, entitlements);
|
||||
let options = getEntOptions(optionsList, priceEnt);
|
||||
if (!options?.quantity) {
|
||||
if (!notNullOrUndefined(options?.quantity)) {
|
||||
throw new RecaseError({
|
||||
message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
|
||||
code: ErrCode.InvalidOptions,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.quantity === 0 && prices.length === 0) {
|
||||
throw new RecaseError({
|
||||
message: `When there's only one price, quantity must be greater than 0`,
|
||||
code: ErrCode.InvalidOptions,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
} else if (billingType === BillingType.UsageBelowThreshold) {
|
||||
let priceEnt = getPriceEntitlement(price, entitlements);
|
||||
let options = getEntOptions(optionsList, priceEnt);
|
||||
@@ -194,23 +206,30 @@ export const handleExistingProduct = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const curPrices =
|
||||
currentProduct?.customer_prices.map((cp: any) => cp.price) || [];
|
||||
// If there's current product and it's not free and new product is a switch
|
||||
if (
|
||||
currentProduct &&
|
||||
!isFreeProduct(curPrices) &&
|
||||
!product.is_add_on &&
|
||||
useCheckout
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Can't use checkout for upgrades / downgrades`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
if (currentProduct && useCheckout) {
|
||||
// If not downgrade to free, throw error
|
||||
let downgradeToFree =
|
||||
!isProductUpgrade(currentProduct.product, product) &&
|
||||
isFreeProduct(attachParams.prices);
|
||||
|
||||
let upgradeFromFree =
|
||||
isProductUpgrade(currentProduct.product, product) &&
|
||||
isFreeProduct(
|
||||
currentProduct?.customer_prices.map((cp: any) => cp.price) || []
|
||||
);
|
||||
|
||||
let isAddOn = attachParams.product.is_add_on;
|
||||
|
||||
if (!downgradeToFree && !upgradeFromFree && !isAddOn) {
|
||||
throw new RecaseError({
|
||||
message: `Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { currentProduct, done: false };
|
||||
return { curCusProduct: currentProduct, done: false };
|
||||
};
|
||||
|
||||
export const checkStripeConnections = async ({
|
||||
@@ -295,6 +314,21 @@ export const checkStripeConnections = async ({
|
||||
await Promise.all(batchPriceUpdates);
|
||||
};
|
||||
|
||||
export const customerHasPm = async ({
|
||||
attachParams,
|
||||
}: {
|
||||
attachParams: AttachParams;
|
||||
}) => {
|
||||
// SCENARIO 3: No payment method, checkout required
|
||||
const paymentMethod = await getCusPaymentMethod({
|
||||
org: attachParams.org,
|
||||
env: attachParams.customer.env,
|
||||
stripeId: attachParams.customer.processor.id,
|
||||
});
|
||||
|
||||
return notNullOrUndefined(paymentMethod) ? true : false;
|
||||
};
|
||||
|
||||
attachRouter.post("/attach", async (req: any, res) => {
|
||||
const {
|
||||
customer_id,
|
||||
@@ -318,14 +352,13 @@ attachRouter.post("/attach", async (req: any, res) => {
|
||||
const optionsListInput: FeatureOptions[] = options || [];
|
||||
const invoiceOnly = invoice_only || false;
|
||||
|
||||
const useCheckout = force_checkout || false;
|
||||
let forceCheckout = force_checkout || false;
|
||||
console.log("--------------------------------");
|
||||
console.log(`ATTACH PRODUCT REQUEST (from ${req.minOrg.slug})`);
|
||||
|
||||
try {
|
||||
z.array(FeatureOptionsSchema).parse(optionsListInput);
|
||||
// 1. Get full customer product data
|
||||
const attachParams = await getFullCusProductData({
|
||||
const attachParams: AttachParams = await getFullCusProductData({
|
||||
sb,
|
||||
customerId: customer_id,
|
||||
productId: product_id,
|
||||
@@ -339,6 +372,23 @@ attachRouter.post("/attach", async (req: any, res) => {
|
||||
isCustom: is_custom,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Customer: ${chalk.yellow(
|
||||
`${attachParams.customer.id} (${attachParams.customer.name})`
|
||||
)}`
|
||||
);
|
||||
|
||||
// 3. Check for stripe connection
|
||||
await checkStripeConnections({ req, res, attachParams });
|
||||
|
||||
let hasPm = await customerHasPm({ attachParams });
|
||||
const useCheckout = !hasPm || forceCheckout;
|
||||
console.log(
|
||||
`Has PM: ${chalk.yellow(hasPm)}, Force Checkout: ${chalk.yellow(
|
||||
forceCheckout
|
||||
)}, Use Checkout: ${chalk.yellow(useCheckout)}`
|
||||
);
|
||||
|
||||
// -------------------- ERROR CHECKING --------------------
|
||||
|
||||
// 1. Check for normal errors (eg. options, different recurring intervals)
|
||||
@@ -347,14 +397,8 @@ attachRouter.post("/attach", async (req: any, res) => {
|
||||
useCheckout,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Customer: ${chalk.yellow(
|
||||
`${attachParams.customer.id} (${attachParams.customer.name})`
|
||||
)}`
|
||||
);
|
||||
|
||||
// 2. Check for existing product and fetch
|
||||
const { currentProduct, done } = await handleExistingProduct({
|
||||
const { curCusProduct, done } = await handleExistingProduct({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
@@ -363,20 +407,18 @@ attachRouter.post("/attach", async (req: any, res) => {
|
||||
|
||||
if (done) return;
|
||||
|
||||
// 3. Check for stripe connection
|
||||
await checkStripeConnections({ req, res, attachParams });
|
||||
|
||||
// -------------------- ATTACH PRODUCT --------------------
|
||||
|
||||
// SCENARIO 1: Free product, no existing product
|
||||
|
||||
const curProductFree = isFreeProduct(
|
||||
currentProduct?.customer_prices.map((cp: any) => cp.price) || [] // if no current product...
|
||||
curCusProduct?.customer_prices.map((cp: any) => cp.price) || [] // if no current product...
|
||||
);
|
||||
|
||||
const newProductFree = isFreeProduct(attachParams.prices);
|
||||
attachParams.curCusProduct = !curProductFree ? curCusProduct : null;
|
||||
|
||||
if (
|
||||
(!currentProduct && newProductFree) ||
|
||||
(!curCusProduct && newProductFree) ||
|
||||
(curProductFree && newProductFree) ||
|
||||
(attachParams.product.is_add_on && newProductFree)
|
||||
) {
|
||||
@@ -395,19 +437,12 @@ attachRouter.post("/attach", async (req: any, res) => {
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
curCusProduct: currentProduct,
|
||||
curCusProduct,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// SCENARIO 3: No payment method, checkout required
|
||||
const paymentMethod = await getCusPaymentMethod({
|
||||
org: attachParams.org,
|
||||
env: attachParams.customer.env,
|
||||
stripeId: attachParams.customer.processor.id,
|
||||
});
|
||||
|
||||
if (!paymentMethod || useCheckout) {
|
||||
if (useCheckout) {
|
||||
console.log("SCENARIO 2: NO PAYMENT METHOD, CHECKOUT REQUIRED");
|
||||
await handleCreateCheckout({
|
||||
sb,
|
||||
@@ -418,13 +453,14 @@ attachRouter.post("/attach", async (req: any, res) => {
|
||||
}
|
||||
|
||||
// SCENARIO 4: Switching product
|
||||
if (!attachParams.product.is_add_on && currentProduct) {
|
||||
|
||||
if (!attachParams.product.is_add_on && curCusProduct) {
|
||||
console.log("SCENARIO 3: SWITCHING PRODUCT (PAYMENT METHOD EXISTS)");
|
||||
await handleChangeProduct({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
curCusProduct: currentProduct,
|
||||
curCusProduct,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -536,12 +536,14 @@ export class CusService {
|
||||
withPrices = false,
|
||||
withProduct = false,
|
||||
inStatuses,
|
||||
productGroup,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
internalCustomerId: string;
|
||||
withProduct?: boolean;
|
||||
withPrices?: boolean;
|
||||
inStatuses?: CusProductStatus[];
|
||||
productGroup?: string;
|
||||
}) {
|
||||
const selectQuery = [
|
||||
"*",
|
||||
@@ -549,7 +551,7 @@ export class CusService {
|
||||
withPrices
|
||||
? "customer_prices:customer_prices(*, price:prices!inner(*))"
|
||||
: "",
|
||||
`customer_entitlements:customer_entitlements!inner(*,
|
||||
`customer_entitlements:customer_entitlements(*,
|
||||
entitlement:entitlements(*,
|
||||
feature:features!inner(*)
|
||||
)
|
||||
@@ -567,6 +569,10 @@ export class CusService {
|
||||
query.in("status", inStatuses);
|
||||
}
|
||||
|
||||
if (productGroup) {
|
||||
query.eq("product.group", productGroup);
|
||||
}
|
||||
|
||||
// query.limit(100);
|
||||
// TODO: Limit 100 cus products? (for one time add ons...)
|
||||
// SORT by created_at?
|
||||
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
FeatureOptions,
|
||||
FreeTrial,
|
||||
BillingType,
|
||||
UsagePriceConfig,
|
||||
CollectionMethod,
|
||||
} from "@autumn/shared";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { getNextEntitlementReset } from "@/utils/timeUtils.js";
|
||||
import { Customer, Feature, FeatureType } from "@autumn/shared";
|
||||
import { Customer, FeatureType } from "@autumn/shared";
|
||||
import { EntitlementWithFeature, FullProduct } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
getBillNowPrices,
|
||||
getPriceEntitlement,
|
||||
getPriceOptions,
|
||||
getStripeSubItems,
|
||||
pricesOnlyOneOff,
|
||||
} from "@/internal/prices/priceUtils.js";
|
||||
|
||||
@@ -20,6 +19,7 @@ import { InvoiceService } from "../invoices/InvoiceService.js";
|
||||
import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
|
||||
import { createStripeSubscription } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { handleCreateCheckout } from "./handleCreateCheckout.js";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripePriceUtils.js";
|
||||
|
||||
const handleBillNowPrices = async ({
|
||||
sb,
|
||||
@@ -34,7 +34,7 @@ const handleBillNowPrices = async ({
|
||||
|
||||
const stripeCli = createStripeCli({ org, env: customer.env });
|
||||
|
||||
const { items, itemMetas } = getStripeSubItems({
|
||||
const { items, itemMetas } = await getStripeSubItems({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
|
||||
import {
|
||||
getStripeSubItems,
|
||||
pricesContainRecurring,
|
||||
} from "@/internal/prices/priceUtils.js";
|
||||
import { pricesContainRecurring } from "@/internal/prices/priceUtils.js";
|
||||
|
||||
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
|
||||
import { AttachParams } from "../products/AttachParams.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
|
||||
import { BillingType, FixedPriceConfig } from "@autumn/shared";
|
||||
import { differenceInDays, format } from "date-fns";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripePriceUtils.js";
|
||||
export const handleCreateCheckout = async ({
|
||||
sb,
|
||||
res,
|
||||
@@ -23,7 +22,7 @@ export const handleCreateCheckout = async ({
|
||||
`Creating checkout for customer ${attachParams.customer.id}, product ${attachParams.product.name}`
|
||||
);
|
||||
|
||||
const { customer, org, freeTrial } = attachParams;
|
||||
const { customer, org, freeTrial, curCusProduct } = attachParams;
|
||||
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
@@ -31,7 +30,7 @@ export const handleCreateCheckout = async ({
|
||||
});
|
||||
|
||||
// Get stripeItems
|
||||
const { items, itemMetas } = getStripeSubItems({
|
||||
const { items, itemMetas } = await getStripeSubItems({
|
||||
attachParams,
|
||||
isCheckout: true,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { createStripeSubscription } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import {
|
||||
getBillNowPrices,
|
||||
getStripeSubItems,
|
||||
pricesOnlyOneOff,
|
||||
} from "@/internal/prices/priceUtils.js";
|
||||
import { createFullCusProduct } from "./createFullCusProduct.js";
|
||||
@@ -18,7 +17,10 @@ import {
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { CusProductService } from "../products/CusProductService.js";
|
||||
import Stripe from "stripe";
|
||||
import { pricesToInvoiceItems } from "@/external/stripe/stripePriceUtils.js";
|
||||
import {
|
||||
getStripeSubItems,
|
||||
pricesToInvoiceItems,
|
||||
} from "@/external/stripe/stripePriceUtils.js";
|
||||
|
||||
export const voidLatestInvoice = async ({
|
||||
stripeCli,
|
||||
@@ -174,7 +176,7 @@ export const handleInvoiceOnly = async ({
|
||||
|
||||
// 1. Create stripe subscription (with invoice)
|
||||
console.log(" - Creating stripe subscription");
|
||||
const { items, itemMetas } = getStripeSubItems({
|
||||
const { items, itemMetas } = await getStripeSubItems({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { getStripeSubItems } from "@/internal/prices/priceUtils.js";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripePriceUtils.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import {
|
||||
isFreeProduct,
|
||||
@@ -16,10 +16,8 @@ import { handleAddProduct } from "../add-product/handleAddProduct.js";
|
||||
import { CusProductService } from "../products/CusProductService.js";
|
||||
import { AttachParams } from "../products/AttachParams.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import chalk from "chalk";
|
||||
import RecaseError, { isPaymentDeclined } from "@/utils/errorUtils.js";
|
||||
import { updateStripeSubscription } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { handleCreateCheckout } from "../add-product/handleCreateCheckout.js";
|
||||
import { InvoiceService } from "../invoices/InvoiceService.js";
|
||||
|
||||
const scheduleStripeSubscription = async ({
|
||||
attachParams,
|
||||
@@ -32,7 +30,7 @@ const scheduleStripeSubscription = async ({
|
||||
}) => {
|
||||
const { org, customer } = attachParams;
|
||||
|
||||
const { items, itemMetas } = getStripeSubItems({
|
||||
const { items, itemMetas } = await getStripeSubItems({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
@@ -151,7 +149,7 @@ const handleStripeSubUpdate = async ({
|
||||
const subscription = await stripeCli.subscriptions.retrieve(subscriptionId);
|
||||
|
||||
// Get stripe subscription from product
|
||||
const { items, itemMetas } = getStripeSubItems({
|
||||
const { items, itemMetas } = await getStripeSubItems({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
@@ -215,10 +213,27 @@ const handleUpgrade = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const disableFreeTrial =
|
||||
curFullProduct.free_trial && org.config?.free_trial_paid_to_paid;
|
||||
// 2. If current product is a trial, just start a new period
|
||||
if (curCusProduct.trial_ends_at && curCusProduct.trial_ends_at > Date.now()) {
|
||||
console.log(
|
||||
"NOTE: Current product is a trial, cancel and start new subscription"
|
||||
);
|
||||
|
||||
// Maybe do it such that if cur cus product has no subscription ID, we just create a new one?
|
||||
await handleAddProduct({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
});
|
||||
|
||||
await stripeCli.subscriptions.cancel(
|
||||
curCusProduct.processor?.subscription_id!
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
// const disableFreeTrial =
|
||||
// curCusProduct.free_trial_id && org.config?.free_trial_paid_to_paid;
|
||||
const disableFreeTrial = false;
|
||||
|
||||
console.log("1. Updating current subscription to new product");
|
||||
let subUpdate;
|
||||
@@ -226,6 +241,7 @@ const handleUpgrade = async ({
|
||||
subscriptionId: curCusProduct.processor?.subscription_id!,
|
||||
stripeCli,
|
||||
attachParams,
|
||||
disableFreeTrial,
|
||||
});
|
||||
|
||||
// Handle backend
|
||||
@@ -240,6 +256,19 @@ const handleUpgrade = async ({
|
||||
disableFreeTrial,
|
||||
});
|
||||
|
||||
// // Insert latest invoice
|
||||
// const stripeInvoice = await stripeCli.invoices.retrieve(
|
||||
// subUpdate.latest_invoice as string
|
||||
// );
|
||||
// await InvoiceService.createInvoiceFromStripe({
|
||||
// sb: req.sb,
|
||||
// stripeInvoice,
|
||||
// internalCustomerId: customer.id,
|
||||
// org: org,
|
||||
// productIds: [product.id],
|
||||
// internalProductIds: [product.id],
|
||||
// });
|
||||
|
||||
res.status(200).json({ success: true, message: "Product change handled" });
|
||||
};
|
||||
|
||||
@@ -259,7 +288,7 @@ export const handleChangeProduct = async ({
|
||||
const { org, customer, product, prices, entitlements, optionsList } =
|
||||
attachParams;
|
||||
|
||||
const curFullProduct = await ProductService.getFullProduct({
|
||||
const curFullProduct = await ProductService.getFullProductStrict({
|
||||
sb: req.sb,
|
||||
productId: curProduct.id,
|
||||
orgId: org.id,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
EntitlementWithFeature,
|
||||
FeatureOptions,
|
||||
FreeTrial,
|
||||
FullCusProduct,
|
||||
FullProduct,
|
||||
Organization,
|
||||
Price,
|
||||
@@ -17,4 +18,6 @@ export type AttachParams = {
|
||||
entitlements: EntitlementWithFeature[];
|
||||
freeTrial: FreeTrial | null;
|
||||
optionsList: FeatureOptions[];
|
||||
|
||||
curCusProduct?: FullCusProduct | undefined;
|
||||
};
|
||||
|
||||
@@ -90,15 +90,29 @@ export class CusProductService {
|
||||
static async getByInternalCusId({
|
||||
sb,
|
||||
cusId,
|
||||
inStatuses,
|
||||
productGroup,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
cusId: string;
|
||||
inStatuses?: string[];
|
||||
productGroup?: string;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
const query = sb
|
||||
.from("customer_products")
|
||||
.select("*, product:products!inner(*)")
|
||||
.eq("internal_customer_id", cusId);
|
||||
|
||||
if (inStatuses) {
|
||||
query.in("status", inStatuses);
|
||||
}
|
||||
|
||||
if (productGroup) {
|
||||
query.eq("product.group", productGroup);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -120,7 +134,7 @@ export class CusProductService {
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
product:products!inner(*),
|
||||
product:products!inner(*, prices(*)),
|
||||
customer_prices:customer_prices(*, price:prices!inner(*))
|
||||
`
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
Organization,
|
||||
AppEnv,
|
||||
FeatureOptions,
|
||||
CusProduct,
|
||||
FullCusProduct,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
|
||||
@@ -1,6 +1,60 @@
|
||||
import { decryptData } from "@/utils/encryptUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
|
||||
import { createSvixApp } from "@/external/svix/svixUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
|
||||
export const initOrgSvixApps = async ({
|
||||
id,
|
||||
slug,
|
||||
}: {
|
||||
id: string;
|
||||
slug: string;
|
||||
}) => {
|
||||
const batchCreate = [];
|
||||
batchCreate.push(
|
||||
createSvixApp({
|
||||
name: `${slug}_${AppEnv.Sandbox}`,
|
||||
orgId: id,
|
||||
env: AppEnv.Sandbox,
|
||||
})
|
||||
);
|
||||
batchCreate.push(
|
||||
createSvixApp({
|
||||
name: `${slug}_${AppEnv.Live}`,
|
||||
orgId: id,
|
||||
env: AppEnv.Live,
|
||||
})
|
||||
);
|
||||
|
||||
const [sandboxApp, liveApp] = await Promise.all(batchCreate);
|
||||
|
||||
return { sandboxApp, liveApp };
|
||||
};
|
||||
|
||||
export const deleteStripeWebhook = async ({
|
||||
org,
|
||||
env,
|
||||
}: {
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
const webhookEndpoints = await stripeCli.webhookEndpoints.list({
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
for (const webhook of webhookEndpoints.data) {
|
||||
if (webhook.url.includes(org.id)) {
|
||||
try {
|
||||
await stripeCli.webhookEndpoints.del(webhook.id);
|
||||
} catch (error: any) {
|
||||
console.log(`Failed to delete stripe webhook (${env}) ${webhook.url}`);
|
||||
console.log(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getStripeWebhookSecret = (org: Organization, env: AppEnv) => {
|
||||
if (!org.stripe_config) {
|
||||
|
||||
@@ -215,44 +215,6 @@ export function compareBillingIntervals(
|
||||
return priority[a] - priority[b];
|
||||
}
|
||||
|
||||
// Stripe items
|
||||
export const getStripeSubItems = ({
|
||||
attachParams,
|
||||
isCheckout = false,
|
||||
}: {
|
||||
attachParams: AttachParams;
|
||||
isCheckout?: boolean;
|
||||
}) => {
|
||||
const { product, prices, entitlements, optionsList, org } = attachParams;
|
||||
// const billNowPrices = getBillNowPrices(prices);
|
||||
const checkoutRelevantPrices = getCheckoutRelevantPrices(prices);
|
||||
|
||||
let subItems: any[] = [];
|
||||
let itemMetas: any[] = [];
|
||||
|
||||
// TODO: Check if non bill now prices can be added to stripe subscription...?
|
||||
for (const price of checkoutRelevantPrices) {
|
||||
const priceEnt = getPriceEntitlement(price, entitlements);
|
||||
const options = getEntOptions(optionsList, priceEnt);
|
||||
|
||||
const { lineItem, lineItemMeta } = priceToStripeItem({
|
||||
price,
|
||||
product,
|
||||
org,
|
||||
options,
|
||||
isCheckout,
|
||||
relatedEnt: priceEnt,
|
||||
});
|
||||
|
||||
subItems.push(lineItem);
|
||||
itemMetas.push(lineItemMeta);
|
||||
}
|
||||
|
||||
console.log("Line items: ", subItems);
|
||||
|
||||
return { items: subItems, itemMetas };
|
||||
};
|
||||
|
||||
export const getUsageTier = (price: Price, quantity: number) => {
|
||||
let usageConfig = price.config as UsagePriceConfig;
|
||||
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
|
||||
|
||||
@@ -197,40 +197,6 @@ export class ProductService {
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getFullProduct({
|
||||
sb,
|
||||
productId,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
productId: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("products")
|
||||
.select(
|
||||
`*,
|
||||
entitlements (
|
||||
*,
|
||||
feature:features (id, name, type)
|
||||
),
|
||||
prices (*)
|
||||
`
|
||||
)
|
||||
.eq("id", productId)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getFullProductStrict({
|
||||
sb,
|
||||
productId,
|
||||
|
||||
@@ -13,6 +13,11 @@ export const StripeConfigSchema = z.object({
|
||||
success_url: z.string(),
|
||||
});
|
||||
|
||||
export const SvixConfigSchema = z.object({
|
||||
sandbox_app_id: z.string(),
|
||||
live_app_id: z.string(),
|
||||
});
|
||||
|
||||
export const OrganizationSchema = z.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
@@ -21,6 +26,12 @@ export const OrganizationSchema = z.object({
|
||||
stripe_config: StripeConfigSchema.optional().nullable(),
|
||||
test_pkey: z.string(),
|
||||
live_pkey: z.string(),
|
||||
created_at: z.number(),
|
||||
|
||||
svix_config: z.object({
|
||||
sandbox_app_id: z.string(),
|
||||
live_app_id: z.string(),
|
||||
}),
|
||||
|
||||
config: z
|
||||
.object({
|
||||
|
||||
Reference in New Issue
Block a user