feat: platform exchange endpoint
This commit is contained in:
1
server/src/external/stripe/stripeCusUtils.ts
vendored
1
server/src/external/stripe/stripeCusUtils.ts
vendored
@@ -111,7 +111,6 @@ export const createStripeCustomer = async ({
|
||||
|
||||
return stripeCustomer;
|
||||
} catch (error: any) {
|
||||
console.log("error", error);
|
||||
throw new RecaseError({
|
||||
message: `Error creating customer in Stripe. ${error.message}`,
|
||||
code: ErrCode.StripeCreateCustomerFailed,
|
||||
|
||||
@@ -66,12 +66,12 @@ export const createStripeSub = 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 {
|
||||
|
||||
18
server/src/external/stripe/utils.ts
vendored
18
server/src/external/stripe/utils.ts
vendored
@@ -20,16 +20,18 @@ export const createStripeCli = ({
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
if (!org.stripe_config) {
|
||||
throw new RecaseError({
|
||||
message: "Stripe config not found",
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
});
|
||||
}
|
||||
let encrypted =
|
||||
env == AppEnv.Sandbox
|
||||
? org.stripe_config.test_api_key
|
||||
: org.stripe_config.live_api_key;
|
||||
? org.stripe_config?.test_api_key
|
||||
: org.stripe_config?.live_api_key;
|
||||
|
||||
if (!encrypted) {
|
||||
throw new RecaseError({
|
||||
message: `Please connect your Stripe ${env == AppEnv.Sandbox ? "test" : "live"} keys. You can find them here: https://dashboard.stripe.com${env == AppEnv.Sandbox ? "/test" : ""}/apikeys`,
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
let decrypted = decryptData(encrypted);
|
||||
return new Stripe(decrypted);
|
||||
|
||||
@@ -63,7 +63,7 @@ export const handlePaidProduct = async ({
|
||||
let mergeCusProduct = undefined;
|
||||
if (!config.disableMerge && !freeTrial) {
|
||||
mergeCusProduct = cusProducts?.find((cp) =>
|
||||
products.some((p) => p.group == cp.product.group),
|
||||
products.some((p) => p.group == cp.product.group)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const handlePaidProduct = async ({
|
||||
}
|
||||
|
||||
let mergeWithSub = mergeSubs.find(
|
||||
(sub) => subToAutumnInterval(sub) == itemSet.interval,
|
||||
(sub) => subToAutumnInterval(sub) == itemSet.interval
|
||||
);
|
||||
|
||||
let subscription;
|
||||
@@ -150,7 +150,7 @@ export const handlePaidProduct = async ({
|
||||
carryExistingUsages: config.carryUsage,
|
||||
scenario: AttachScenario.New,
|
||||
logger,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(batchInsert);
|
||||
@@ -163,7 +163,7 @@ export const handlePaidProduct = async ({
|
||||
invoiceId: sub.latest_invoice as string,
|
||||
attachParams,
|
||||
logger,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
const invoices = await Promise.all(batchInsertInvoice);
|
||||
@@ -180,7 +180,7 @@ export const handlePaidProduct = async ({
|
||||
product_ids: products.map((p) => p.id),
|
||||
customer_id: customer.id || customer.internal_id,
|
||||
invoice: invoiceOnly ? invoices?.[0] : undefined,
|
||||
}),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
res.status(200).json({
|
||||
|
||||
@@ -49,6 +49,7 @@ export const getPricesAndEnts = async ({
|
||||
|
||||
let freeTrial = null;
|
||||
let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
|
||||
// freeTrial = freeTrialProduct?.free_trial;
|
||||
if (freeTrialProduct) {
|
||||
freeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
@@ -60,8 +61,6 @@ export const getPricesAndEnts = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const prodIsMain = isMainProduct({ product: products[0], prices });
|
||||
|
||||
return {
|
||||
optionsList: mapOptionsList({
|
||||
optionsInput: optionsInput || [],
|
||||
|
||||
@@ -57,20 +57,13 @@ export const processAttachBody = async ({
|
||||
// 1. Get customer and products
|
||||
const { org, env } = req;
|
||||
|
||||
if (!org.stripe_connected) {
|
||||
throw new RecaseError({
|
||||
message: "Please connect to Stripe to add products",
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
|
||||
const { customer, products } = await getCustomerAndProducts({
|
||||
req,
|
||||
attachBody,
|
||||
});
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
const [stripeCusData, rewardData] = await Promise.all([
|
||||
getStripeCusData({
|
||||
stripeCli,
|
||||
|
||||
@@ -88,6 +88,7 @@ export const attachParamsToPreview = async ({
|
||||
branch,
|
||||
now,
|
||||
withPrepaid,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
getFirstInterval,
|
||||
getLastInterval,
|
||||
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { getFirstInterval } 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";
|
||||
@@ -18,9 +15,11 @@ import {
|
||||
AttachBranch,
|
||||
BillingInterval,
|
||||
FreeTrial,
|
||||
FullCusProduct,
|
||||
PreviewLineItem,
|
||||
Price,
|
||||
UsageModel,
|
||||
AttachConfig,
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
addBillingIntervalUnix,
|
||||
@@ -39,6 +38,8 @@ const getNextCycleAt = ({
|
||||
interval,
|
||||
now,
|
||||
freeTrial,
|
||||
branch,
|
||||
curCusProduct,
|
||||
}: {
|
||||
prices: Price[];
|
||||
stripeSubs: Stripe.Subscription[];
|
||||
@@ -46,9 +47,17 @@ const getNextCycleAt = ({
|
||||
interval: BillingInterval;
|
||||
now?: number;
|
||||
freeTrial?: FreeTrial | null;
|
||||
branch: AttachBranch;
|
||||
curCusProduct?: FullCusProduct;
|
||||
}) => {
|
||||
now = now || Date.now();
|
||||
|
||||
if (branch == AttachBranch.NewVersion && curCusProduct?.free_trial) {
|
||||
return {
|
||||
next_cycle_at: curCusProduct.trial_ends_at,
|
||||
};
|
||||
}
|
||||
|
||||
if (freeTrial) {
|
||||
return {
|
||||
next_cycle_at:
|
||||
@@ -85,12 +94,14 @@ export const getUpgradeProductPreview = async ({
|
||||
branch,
|
||||
now,
|
||||
withPrepaid = false,
|
||||
config,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachParams: AttachParams;
|
||||
branch: AttachBranch;
|
||||
now: number;
|
||||
withPrepaid?: boolean;
|
||||
config?: AttachConfig;
|
||||
}) => {
|
||||
const { logtail: logger } = req;
|
||||
|
||||
@@ -123,18 +134,22 @@ export const getUpgradeProductPreview = async ({
|
||||
? stripeSubs[0].current_period_end * 1000
|
||||
: undefined;
|
||||
|
||||
let freeTrial = attachParams.freeTrial;
|
||||
if (config?.carryTrial && curCusProduct?.free_trial) {
|
||||
freeTrial = curCusProduct.free_trial;
|
||||
}
|
||||
|
||||
const newPreviewItems = await getItemsForNewProduct({
|
||||
newProduct,
|
||||
attachParams,
|
||||
now,
|
||||
anchorToUnix,
|
||||
freeTrial: attachParams.freeTrial,
|
||||
freeTrial,
|
||||
stripeSubs,
|
||||
logger,
|
||||
withPrepaid,
|
||||
});
|
||||
|
||||
// const lastInterval = getLastInterval({ prices: newProduct.prices });
|
||||
const lastInterval = getFirstInterval({ prices: newProduct.prices });
|
||||
|
||||
let dueNextCycle = undefined;
|
||||
@@ -146,6 +161,8 @@ export const getUpgradeProductPreview = async ({
|
||||
interval: lastInterval,
|
||||
now,
|
||||
freeTrial: attachParams.freeTrial,
|
||||
branch,
|
||||
curCusProduct,
|
||||
});
|
||||
|
||||
let nextCycleItems = await getItemsForNewProduct({
|
||||
|
||||
@@ -78,18 +78,6 @@ export const getCustomerDetails = async ({
|
||||
(cp: FullCusProduct) => cp.subscription_ids || []
|
||||
);
|
||||
|
||||
// if (org.config.api_version >= BREAK_API_VERSION && org.stripe_connected) {
|
||||
// let stripeCli = createStripeCli({
|
||||
// org,
|
||||
// env,
|
||||
// });
|
||||
|
||||
// subs = await getStripeSubs({
|
||||
// stripeCli,
|
||||
// subIds,
|
||||
// expand: withRewards ? ["discounts"] : undefined,
|
||||
// });
|
||||
// }
|
||||
const subs = customer.subscriptions || [];
|
||||
const { main, addOns } = await processFullCusProducts({
|
||||
fullCusProducts: cusProducts,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { CusProductStatus, ErrCode, FullCusProduct } from "@autumn/shared";
|
||||
import { ErrCode, FullCusProduct } from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import { expireCusProduct } from "../handlers/handleCusProductExpired.js";
|
||||
import { RELEVANT_STATUSES } from "../cusProducts/CusProductService.js";
|
||||
@@ -55,6 +54,8 @@ expireRouter.post("", async (req, res) =>
|
||||
});
|
||||
}
|
||||
|
||||
// Handle case if there are two products to expire...
|
||||
|
||||
for (const cusProduct of cusProductsToExpire) {
|
||||
await expireCusProduct({
|
||||
req,
|
||||
|
||||
@@ -60,6 +60,7 @@ export class ApiKeyService {
|
||||
org,
|
||||
features: (data.org.features || []) as Feature[],
|
||||
env,
|
||||
userId: data.user_id,
|
||||
};
|
||||
|
||||
// console.log("result", result);
|
||||
|
||||
@@ -35,6 +35,7 @@ export const createKey = async ({
|
||||
db,
|
||||
env,
|
||||
name,
|
||||
userId,
|
||||
orgId,
|
||||
prefix,
|
||||
meta,
|
||||
@@ -45,6 +46,7 @@ export const createKey = async ({
|
||||
orgId: string;
|
||||
prefix: string;
|
||||
meta: any;
|
||||
userId?: string;
|
||||
}) => {
|
||||
const apiKey = generateApiKey(42, prefix);
|
||||
const hashedKey = hashApiKey(apiKey);
|
||||
@@ -52,7 +54,7 @@ export const createKey = async ({
|
||||
const apiKeyData: ApiKey = {
|
||||
id: generateId("key"),
|
||||
org_id: orgId,
|
||||
user_id: "",
|
||||
user_id: userId || null,
|
||||
name,
|
||||
prefix: apiKey.substring(0, 14),
|
||||
created_at: Date.now(),
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "@/external/stripe/stripeOnboardingUtils.js";
|
||||
import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js";
|
||||
import * as crypto from "crypto";
|
||||
import { isStripeConnected } from "../orgs/orgUtils.js";
|
||||
|
||||
export const devRouter: Router = Router();
|
||||
|
||||
@@ -64,6 +65,7 @@ devRouter.post("/api_key", withOrgAuth, async (req: any, res) =>
|
||||
env,
|
||||
name,
|
||||
orgId,
|
||||
userId: req.user?.id,
|
||||
prefix,
|
||||
meta: {},
|
||||
});
|
||||
@@ -222,6 +224,7 @@ export const handleGetOtp = async (req: any, res: any) =>
|
||||
fromCli: true,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
userId: req.user?.id,
|
||||
});
|
||||
|
||||
const prodKey = await createKey({
|
||||
@@ -234,6 +237,7 @@ export const handleGetOtp = async (req: any, res: any) =>
|
||||
fromCli: true,
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
userId: req.user?.id,
|
||||
});
|
||||
|
||||
// console.log("New keys created:");
|
||||
@@ -245,7 +249,7 @@ export const handleGetOtp = async (req: any, res: any) =>
|
||||
orgId: cacheData.orgId,
|
||||
});
|
||||
|
||||
let stripeConnected = org.stripe_connected;
|
||||
let stripeConnected = isStripeConnected({ org });
|
||||
|
||||
let responseData = {
|
||||
...cacheData,
|
||||
|
||||
@@ -16,8 +16,57 @@ import { OrgService } from "../OrgService.js";
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import { clearOrgCache } from "../orgUtils/clearOrgCache.js";
|
||||
import { disconnectStripe } from "./handleDeleteStripe.js";
|
||||
|
||||
export const connectStripe = async ({
|
||||
db,
|
||||
orgId,
|
||||
logger,
|
||||
apiKey,
|
||||
env,
|
||||
}: {
|
||||
db: any;
|
||||
orgId: string;
|
||||
logger: any;
|
||||
apiKey: string;
|
||||
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
|
||||
const curWebhooks = await stripe.webhookEndpoints.list();
|
||||
for (const webhook of curWebhooks.data) {
|
||||
if (webhook.url.includes(orgId)) {
|
||||
await stripe.webhookEndpoints.del(webhook.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create webhook endpoint
|
||||
let webhook = await createWebhookEndpoint(apiKey, env, orgId);
|
||||
|
||||
// 3. Return encrypted
|
||||
if (env === AppEnv.Sandbox) {
|
||||
return {
|
||||
test_api_key: encryptData(apiKey),
|
||||
test_webhook_secret: encryptData(webhook.secret as string),
|
||||
env,
|
||||
defaultCurrency: account.default_currency,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
live_api_key: encryptData(apiKey),
|
||||
live_webhook_secret: encryptData(webhook.secret as string),
|
||||
env,
|
||||
stripeCurrency: account.default_currency,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const connectAllStripe = async ({
|
||||
db,
|
||||
orgId,
|
||||
logger,
|
||||
@@ -114,7 +163,7 @@ export const handleConnectStripe = async (req: any, res: any) =>
|
||||
}
|
||||
|
||||
let { defaultCurrency: finalDefaultCurrency, stripeConfig } =
|
||||
await connectStripe({
|
||||
await connectAllStripe({
|
||||
db,
|
||||
orgId,
|
||||
logger,
|
||||
|
||||
@@ -3,24 +3,28 @@ import { OrgService } from "../OrgService.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { clearOrgCache } from "../orgUtils/clearOrgCache.js";
|
||||
import { AppEnv, Organization } from "@autumn/shared";
|
||||
import { isStripeConnected } from "../orgUtils.js";
|
||||
|
||||
export const disconnectStripe = async (org: Organization) => {
|
||||
if (isStripeConnected({ org, env: AppEnv.Sandbox })) {
|
||||
const testStripeCli = createStripeCli({ org, env: AppEnv.Sandbox });
|
||||
const liveStripeCli = createStripeCli({ org, env: AppEnv.Live });
|
||||
|
||||
const testWebhooks = await testStripeCli.webhookEndpoints.list();
|
||||
for (const webhook of testWebhooks.data) {
|
||||
if (webhook.url.includes(org.id)) {
|
||||
await testStripeCli.webhookEndpoints.del(webhook.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isStripeConnected({ org, env: AppEnv.Live })) {
|
||||
const liveStripeCli = createStripeCli({ org, env: AppEnv.Live });
|
||||
const liveWebhooks = await liveStripeCli.webhookEndpoints.list();
|
||||
for (const webhook of liveWebhooks.data) {
|
||||
if (webhook.url.includes(org.id)) {
|
||||
await liveStripeCli.webhookEndpoints.del(webhook.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const handleDeleteStripe = async (req: any, res: any) =>
|
||||
|
||||
@@ -4,7 +4,26 @@ import { AppEnv, ErrCode, FrontendOrg, Organization } from "@autumn/shared";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { OrgService } from "./OrgService.js";
|
||||
import { FeatureService } from "../features/FeatureService.js";
|
||||
import { createSvixApp } from "@/external/svix/svixHelpers.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
export const isStripeConnected = ({
|
||||
org,
|
||||
env,
|
||||
}: {
|
||||
org: Organization;
|
||||
env?: AppEnv;
|
||||
}) => {
|
||||
if (env === AppEnv.Sandbox) {
|
||||
return notNullish(org.stripe_config?.test_api_key);
|
||||
} else if (env === AppEnv.Live) {
|
||||
return notNullish(org.stripe_config?.live_api_key);
|
||||
} else {
|
||||
return (
|
||||
notNullish(org.stripe_config?.test_api_key) &&
|
||||
notNullish(org.stripe_config?.live_api_key)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
|
||||
return {
|
||||
@@ -49,19 +68,19 @@ export const deleteStripeWebhook = async ({
|
||||
};
|
||||
|
||||
export const getStripeWebhookSecret = (org: Organization, env: AppEnv) => {
|
||||
if (!org.stripe_config) {
|
||||
const webhookSecret =
|
||||
env === AppEnv.Sandbox
|
||||
? org.stripe_config?.test_webhook_secret
|
||||
: org.stripe_config?.live_webhook_secret;
|
||||
|
||||
if (!webhookSecret) {
|
||||
throw new RecaseError({
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
message: `Stripe config not found for org ${org.id}`,
|
||||
message: `Stripe webhook secret not found for org ${org.id}`,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const webhookSecret =
|
||||
env === AppEnv.Sandbox
|
||||
? org.stripe_config.test_webhook_secret
|
||||
: org.stripe_config!.live_webhook_secret;
|
||||
|
||||
return decryptData(webhookSecret);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
import { generateId } from "better-auth";
|
||||
import { NextFunction, Router } from "express";
|
||||
import { member, organizations, user as userTable } from "@autumn/shared";
|
||||
|
||||
import {
|
||||
AppEnv,
|
||||
member,
|
||||
Organization,
|
||||
organizations,
|
||||
StripeConfig,
|
||||
user as userTable,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { slugify } from "@/utils/genUtils.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { connectStripe } from "../orgs/handlers/handleConnectStripe.js";
|
||||
import { z } from "zod";
|
||||
import { createKey } from "../dev/api-keys/apiKeyUtils.js";
|
||||
import { afterOrgCreated } from "@/utils/authUtils/afterOrgCreated.js";
|
||||
import { Autumn } from "autumn-js";
|
||||
|
||||
const platformRouter = Router();
|
||||
|
||||
const platformAuthMiddleware = (req: any, res: any, next: NextFunction) => {
|
||||
const platformAuthMiddleware = async (
|
||||
req: any,
|
||||
res: any,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (!process.env.AUTUMN_SECRET_KEY) next();
|
||||
|
||||
try {
|
||||
let autumn = new Autumn();
|
||||
const { data, error } = await autumn.check({
|
||||
customer_id: req.org.id,
|
||||
feature_id: "platform",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!data?.allowed) {
|
||||
res.status(403).json({
|
||||
message:
|
||||
"You're not allowed to access the platform API. Please contact hey@useautumn.com to request access!",
|
||||
code: "not_allowed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
message: "Failed to check if org is allowed to access platform",
|
||||
code: "internal_error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
platformRouter.use(platformAuthMiddleware);
|
||||
|
||||
const ExchangeSchema = z.object({
|
||||
organization: z.string(),
|
||||
email: z.string(),
|
||||
stripe_test_key: z.string().nonempty(),
|
||||
stripe_live_key: z.string().nonempty(),
|
||||
email: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
|
||||
stripe_test_key: z.string().nonempty().optional(),
|
||||
stripe_live_key: z.string().nonempty().optional(),
|
||||
});
|
||||
|
||||
platformRouter.post("/exchange", (req: any, res: any) =>
|
||||
@@ -31,57 +73,172 @@ platformRouter.post("/exchange", (req: any, res: any) =>
|
||||
handler: async (req: ExtendedRequest, res: any) => {
|
||||
let { organization, email, stripe_test_key, stripe_live_key } = req.body;
|
||||
|
||||
// 1. Create user with email
|
||||
const { db } = req;
|
||||
const { db, logger } = req;
|
||||
|
||||
// let user = await db.insert(userTable).values({
|
||||
// id: generateId(),
|
||||
// name: "",
|
||||
// email,
|
||||
// emailVerified: true,
|
||||
// createdAt: new Date(),
|
||||
// updatedAt: new Date(),
|
||||
// role: "user",
|
||||
// banned: false,
|
||||
// banReason: null,
|
||||
// banExpires: null,
|
||||
// });
|
||||
ExchangeSchema.parse({
|
||||
organization,
|
||||
email,
|
||||
stripe_test_key,
|
||||
stripe_live_key,
|
||||
});
|
||||
|
||||
// let { defaultCurrency, stripeConfig } = await connectStripe({
|
||||
// db,
|
||||
// orgId: generateId(),
|
||||
// logger: req.logtail,
|
||||
// testApiKey: stripe_test_key,
|
||||
// liveApiKey: stripe_live_key,
|
||||
// successUrl: "https://useautumn.com",
|
||||
// });
|
||||
// 1. Check if user with this email already exists
|
||||
let user = await db.query.user.findFirst({
|
||||
where: eq(userTable.email, email),
|
||||
});
|
||||
|
||||
// // 2. Create org
|
||||
// let orgId = generateId();
|
||||
// await db.insert(organizations).values({
|
||||
// id: orgId,
|
||||
// slug: `${slugify(organization)}_${Math.floor(10000000 + Math.random() * 90000000)}`,
|
||||
// name: organization,
|
||||
// logo: "",
|
||||
// createdAt: new Date(),
|
||||
// metadata: "",
|
||||
// stripe_connected: true,
|
||||
// default_currency: defaultCurrency,
|
||||
// stripe_config: stripeConfig,
|
||||
// });
|
||||
if (!user) {
|
||||
[user] = await db
|
||||
.insert(userTable)
|
||||
.values({
|
||||
id: generateId(),
|
||||
name: "",
|
||||
email,
|
||||
emailVerified: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
role: "user",
|
||||
banned: false,
|
||||
banReason: null,
|
||||
banExpires: null,
|
||||
createdBy: req.org.id,
|
||||
})
|
||||
.returning();
|
||||
}
|
||||
|
||||
// // 3. Create membership
|
||||
// await db.insert(member).values({
|
||||
// id: generateId(),
|
||||
// organizationId: orgId,
|
||||
// userId: user!.id,
|
||||
// role: "owner",
|
||||
// createdAt: new Date(),
|
||||
// });
|
||||
logger.info(`User found / created: ${user.id} (${email})`);
|
||||
|
||||
let org: Organization;
|
||||
|
||||
let membership = await db.query.member.findFirst({
|
||||
where: and(eq(member.userId, user.id!), eq(member.role, "owner")),
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
logger.info(`Connected to Stripe`);
|
||||
|
||||
// 2. Create org
|
||||
let orgId = generateId();
|
||||
|
||||
[org] = (await db
|
||||
.insert(organizations)
|
||||
.values({
|
||||
id: orgId,
|
||||
slug: `platform_org_${Math.floor(10000000 + Math.random() * 90000000)}`,
|
||||
name: `Platform Org`,
|
||||
logo: "",
|
||||
createdAt: new Date(),
|
||||
metadata: "",
|
||||
})
|
||||
.returning()) as [Organization];
|
||||
|
||||
await db.insert(member).values({
|
||||
id: generateId(),
|
||||
organizationId: orgId,
|
||||
userId: user.id!,
|
||||
role: "owner",
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await afterOrgCreated({ org });
|
||||
} else {
|
||||
org = (await db.query.organizations.findFirst({
|
||||
where: eq(organizations.id, membership.organizationId),
|
||||
})) as Organization;
|
||||
}
|
||||
|
||||
let sandboxKey, prodKey;
|
||||
|
||||
let finalStripeConfig: any = {};
|
||||
let defaultCurrency = org.default_currency || "usd";
|
||||
|
||||
if (stripe_test_key) {
|
||||
let { test_api_key, test_webhook_secret, stripeCurrency } =
|
||||
await connectStripe({
|
||||
db,
|
||||
orgId: org.id,
|
||||
logger: req.logtail,
|
||||
apiKey: stripe_test_key,
|
||||
env: AppEnv.Sandbox,
|
||||
});
|
||||
sandboxKey = await createKey({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env: AppEnv.Sandbox,
|
||||
name: "Platform API Key",
|
||||
prefix: "am_sk_test",
|
||||
meta: {},
|
||||
});
|
||||
finalStripeConfig = {
|
||||
...finalStripeConfig,
|
||||
test_api_key,
|
||||
test_webhook_secret,
|
||||
};
|
||||
|
||||
if (!defaultCurrency) {
|
||||
defaultCurrency = stripeCurrency || "usd";
|
||||
}
|
||||
}
|
||||
|
||||
if (stripe_live_key) {
|
||||
let { live_api_key, live_webhook_secret, stripeCurrency } =
|
||||
await connectStripe({
|
||||
db,
|
||||
orgId: org.id,
|
||||
logger: req.logtail,
|
||||
apiKey: stripe_live_key,
|
||||
env: AppEnv.Live,
|
||||
});
|
||||
|
||||
prodKey = await createKey({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env: AppEnv.Live,
|
||||
name: "Platform API Key",
|
||||
prefix: "am_sk_live",
|
||||
meta: {},
|
||||
});
|
||||
|
||||
finalStripeConfig = {
|
||||
...finalStripeConfig,
|
||||
live_api_key,
|
||||
live_webhook_secret,
|
||||
};
|
||||
|
||||
if (!defaultCurrency) {
|
||||
defaultCurrency = stripeCurrency || "usd";
|
||||
}
|
||||
}
|
||||
|
||||
if (!org.stripe_config?.success_url) {
|
||||
finalStripeConfig.success_url = `https://useautumn.com`;
|
||||
}
|
||||
|
||||
await db
|
||||
.update(organizations)
|
||||
.set({
|
||||
default_currency: defaultCurrency,
|
||||
stripe_connected: true,
|
||||
stripe_config: {
|
||||
...org.stripe_config,
|
||||
...finalStripeConfig,
|
||||
} as StripeConfig,
|
||||
})
|
||||
.where(eq(organizations.id, org.id));
|
||||
res.status(200).json({
|
||||
// message: "User created",
|
||||
// user,
|
||||
// org: {
|
||||
// id: org.id,
|
||||
// slug: org.slug,
|
||||
// name: org.name,
|
||||
// },
|
||||
// user: {
|
||||
// id: user.id!,
|
||||
// email,
|
||||
// },
|
||||
api_keys: {
|
||||
sandbox: sandboxKey,
|
||||
production: prodKey,
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
isPriceItem,
|
||||
} from "../../product-items/productItemUtils/getItemType.js";
|
||||
import { isFreeProduct } from "../../productUtils.js";
|
||||
import { isStripeConnected } from "@/internal/orgs/orgUtils.js";
|
||||
|
||||
const productDetailsSame = (prod1: Product, prod2: UpdateProduct) => {
|
||||
if (notNullish(prod2.id) && prod1.id != prod2.id) {
|
||||
@@ -59,7 +60,7 @@ const updateStripeProductNames = async ({
|
||||
newName: string;
|
||||
logger: any;
|
||||
}) => {
|
||||
if (!org.stripe_connected) return;
|
||||
if (!isStripeConnected({ org, env: curProduct.env as AppEnv })) return;
|
||||
const stripeCli = createStripeCli({
|
||||
org,
|
||||
env: curProduct.env as AppEnv,
|
||||
@@ -81,7 +82,7 @@ const updateStripeProductNames = async ({
|
||||
error,
|
||||
stripeProdId,
|
||||
newName,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ const updateStripeProductNames = async ({
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Error updating price ${price.id} name in Stripe: ${error.message}`,
|
||||
`Error updating price ${price.id} name in Stripe: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -198,7 +199,7 @@ export const handleUpdateProductDetails = async ({
|
||||
// Update product name in Stripe
|
||||
if (curProduct.name !== newProduct.name && notNullish(newProduct.name)) {
|
||||
logger.info(
|
||||
`Updating product (${curProduct.id}) name in Stripe to ${newProduct.name}`,
|
||||
`Updating product (${curProduct.id}) name in Stripe to ${newProduct.name}`
|
||||
);
|
||||
await updateStripeProductNames({
|
||||
db,
|
||||
|
||||
@@ -12,7 +12,7 @@ const handleResFinish = (req: any, res: any) => {
|
||||
{
|
||||
statusCode: res.statusCode,
|
||||
res: res.locals.responseBody,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -29,7 +29,7 @@ const parseCustomerIdFromUrl = (url: string): string | undefined => {
|
||||
const cleanUrl = url.split("?")[0].replace(/^\/+|\/+$/g, "");
|
||||
const segments = cleanUrl.split("/");
|
||||
const customersIndex = segments.findIndex(
|
||||
(segment) => segment === "customers",
|
||||
(segment) => segment === "customers"
|
||||
);
|
||||
|
||||
if (customersIndex !== -1 && segments[customersIndex + 1]) {
|
||||
@@ -48,6 +48,7 @@ export const analyticsMiddleware = async (req: any, res: any, next: any) => {
|
||||
body: req.body,
|
||||
customer_id:
|
||||
req?.body?.customer_id || parseCustomerIdFromUrl(req.originalUrl),
|
||||
user_id: req.userId || null,
|
||||
};
|
||||
|
||||
if (req.span) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
let { org, features, env } = data;
|
||||
let { org, features, env, userId } = data;
|
||||
req.orgId = org.id;
|
||||
req.env = env;
|
||||
req.minOrg = {
|
||||
@@ -90,6 +90,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => {
|
||||
req.org = org;
|
||||
req.features = features;
|
||||
req.authType = AuthType.SecretKey;
|
||||
req.userId = userId;
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../../external/stripe/stripeCusUtils.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import Stripe from "stripe";
|
||||
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
|
||||
export const createCusInStripe = async ({
|
||||
customer,
|
||||
@@ -83,6 +84,12 @@ export const initCustomer = async ({
|
||||
|
||||
if (customer) {
|
||||
await autumn.customers.delete(customerId);
|
||||
await deleteCusCache({
|
||||
db,
|
||||
customerId: customerId,
|
||||
orgId: org.id,
|
||||
env: env,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -95,6 +102,9 @@ export const initCustomer = async ({
|
||||
env: env,
|
||||
})) as Customer;
|
||||
|
||||
// console.log("customer id", customerId);
|
||||
// console.log("org id", org.id);
|
||||
// console.log("env", env);
|
||||
// console.log("customer", customer);
|
||||
|
||||
const stripeCli = createStripeCli({ org: org, env: env });
|
||||
|
||||
@@ -130,8 +130,9 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to
|
||||
testClockId,
|
||||
advanceTo: addHours(
|
||||
addMonths(curUnix, 1),
|
||||
hoursToFinalizeInvoice,
|
||||
hoursToFinalizeInvoice
|
||||
).getTime(),
|
||||
waitForSeconds: 30,
|
||||
});
|
||||
|
||||
await expectInvoiceAfterUsage({
|
||||
|
||||
168
server/tests/attach/newVersion/newVersion2.ts
Normal file
168
server/tests/attach/newVersion/newVersion2.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { expect } from "chai";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
APIVersion,
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
Organization,
|
||||
ProductV2,
|
||||
} 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 { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { replaceItems } from "../utils.js";
|
||||
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
|
||||
import runUpdateEntsTest from "../updateEnts/expectUpdateEnts.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addHours, addMonths, addWeeks } from "date-fns";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "pro",
|
||||
trial: true,
|
||||
});
|
||||
|
||||
const testCase = "newVersion2";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for trial product`)}`, () => {
|
||||
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;
|
||||
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach pro product", async function () {
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
let usage = 50000;
|
||||
let newPro: ProductV2;
|
||||
it("should update product to new version", async function () {
|
||||
newPro = structuredClone(pro);
|
||||
let newItems = replaceItems({
|
||||
items: pro.items,
|
||||
interval: BillingInterval.Month,
|
||||
newItem: constructPriceItem({
|
||||
price: 100,
|
||||
interval: BillingInterval.Month,
|
||||
}),
|
||||
});
|
||||
|
||||
newPro.version = 2;
|
||||
newPro.items = newItems;
|
||||
|
||||
await autumn.products.update(pro.id, {
|
||||
items: newItems,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
it("should attach pro v2", async function () {
|
||||
await runUpdateEntsTest({
|
||||
autumn,
|
||||
stripeCli,
|
||||
customerId,
|
||||
customProduct: newPro,
|
||||
newVersion: 2,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
// it("should have correct invoice total on next cycle", async function () {
|
||||
// const invoiceTotal = await getExpectedInvoiceTotal({
|
||||
// org,
|
||||
// env,
|
||||
// customerId,
|
||||
// productId: pro.id,
|
||||
// stripeCli,
|
||||
// db,
|
||||
// usage: [
|
||||
// {
|
||||
// featureId: TestFeature.Words,
|
||||
// value: usage,
|
||||
// },
|
||||
// ],
|
||||
// onlyIncludeMonthly: true,
|
||||
// });
|
||||
|
||||
// let curUnix = Date.now();
|
||||
// curUnix = await advanceTestClock({
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// advanceTo: addMonths(curUnix, 1).getTime(),
|
||||
// waitForSeconds: 30,
|
||||
// });
|
||||
|
||||
// await advanceTestClock({
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
|
||||
// waitForSeconds: 10,
|
||||
// });
|
||||
|
||||
// const customer = await autumn.customers.get(customerId);
|
||||
// const invoice = customer.invoices[0];
|
||||
// expect(invoice.total).to.equal(
|
||||
// invoiceTotal,
|
||||
// "invoice total after 1 cycle should be correct"
|
||||
// );
|
||||
// });
|
||||
});
|
||||
@@ -4,10 +4,13 @@ import {
|
||||
timestamp,
|
||||
boolean,
|
||||
integer,
|
||||
foreignKey,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { organizations } from "./schema.js";
|
||||
|
||||
export const user = pgTable("user", {
|
||||
export const user = pgTable(
|
||||
"user",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
@@ -25,7 +28,16 @@ export const user = pgTable("user", {
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
});
|
||||
createdBy: text("created_by"),
|
||||
},
|
||||
(table) => [
|
||||
foreignKey({
|
||||
columns: [table.createdBy],
|
||||
foreignColumns: [organizations.id],
|
||||
name: "user_created_by_fkey",
|
||||
}),
|
||||
]
|
||||
);
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: text("id").primaryKey(),
|
||||
@@ -66,10 +78,10 @@ export const verification = pgTable("verification", {
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).$defaultFn(
|
||||
() => /* @__PURE__ */ new Date(),
|
||||
() => /* @__PURE__ */ new Date()
|
||||
),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).$defaultFn(
|
||||
() => /* @__PURE__ */ new Date(),
|
||||
() => /* @__PURE__ */ new Date()
|
||||
),
|
||||
}).enableRLS();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AppEnv } from "../genModels/genEnums.js";
|
||||
export type ApiKey = {
|
||||
id: string;
|
||||
org_id: string;
|
||||
user_id: string;
|
||||
user_id: string | null;
|
||||
name: string;
|
||||
prefix: string;
|
||||
created_at: number;
|
||||
|
||||
@@ -17,11 +17,11 @@ export type SvixConfig = {
|
||||
};
|
||||
|
||||
export type StripeConfig = {
|
||||
test_api_key: string;
|
||||
live_api_key: string;
|
||||
test_webhook_secret: string;
|
||||
live_webhook_secret: string;
|
||||
success_url: string;
|
||||
test_api_key?: string;
|
||||
live_api_key?: string;
|
||||
test_webhook_secret?: string;
|
||||
live_webhook_secret?: string;
|
||||
success_url?: string;
|
||||
};
|
||||
|
||||
// logo: text("logo"),
|
||||
@@ -56,7 +56,7 @@ export const organizations = pgTable(
|
||||
(table) => [
|
||||
unique("organizations_test_pkey_key").on(table.test_pkey),
|
||||
unique("organizations_live_pkey_key").on(table.live_pkey),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
export type Organization = typeof organizations.$inferSelect & {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CusProductStripeLink } from "./CusProductStripeLink";
|
||||
import { keyToTitle } from "@/utils/formatUtils/formatTextUtils";
|
||||
import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { notNullish } from "@/utils/genUtils";
|
||||
import { differenceInDays, subDays } from "date-fns";
|
||||
|
||||
export const CusProductStatusItem = ({
|
||||
cusProduct,
|
||||
@@ -12,12 +13,14 @@ export const CusProductStatusItem = ({
|
||||
cusProduct: FullCusProduct;
|
||||
}) => {
|
||||
const getStatus = () => {
|
||||
if (cusProduct.status == CusProductStatus.Expired) {
|
||||
return CusProductStatus.Expired;
|
||||
}
|
||||
|
||||
const trialing =
|
||||
cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
|
||||
|
||||
const canceled =
|
||||
notNullish(cusProduct.canceled_at) &&
|
||||
cusProduct.status !== CusProductStatus.Expired;
|
||||
const canceled = notNullish(cusProduct.canceled_at);
|
||||
if (canceled) return "canceled";
|
||||
|
||||
if (trialing) {
|
||||
@@ -27,14 +30,24 @@ export const CusProductStatusItem = ({
|
||||
return cusProduct.status;
|
||||
};
|
||||
|
||||
const isCanceled = notNullish(cusProduct.canceled_at);
|
||||
const getTitle = () => {
|
||||
const status = getStatus();
|
||||
if (status == CusProductStatus.Trialing) {
|
||||
const daysTillEnd = differenceInDays(
|
||||
new Date(cusProduct.trial_ends_at!),
|
||||
new Date()
|
||||
);
|
||||
return `trial (${daysTillEnd}d)`;
|
||||
}
|
||||
return keyToTitle(getStatus()).toLowerCase();
|
||||
};
|
||||
|
||||
const statusToColor: Record<CusProductStatus | "canceled", string> = {
|
||||
[CusProductStatus.Active]: "bg-lime-500",
|
||||
[CusProductStatus.Expired]: "bg-stone-800",
|
||||
[CusProductStatus.PastDue]: "bg-red-500",
|
||||
[CusProductStatus.Scheduled]: "bg-blue-500",
|
||||
[CusProductStatus.Trialing]: "bg-yellow-400",
|
||||
[CusProductStatus.Trialing]: "bg-blue-400",
|
||||
canceled: "bg-gray-500",
|
||||
[CusProductStatus.Unknown]: "bg-gray-500",
|
||||
};
|
||||
@@ -45,7 +58,7 @@ export const CusProductStatusItem = ({
|
||||
variant="status"
|
||||
className={cn("h-fit", statusToColor[getStatus()])}
|
||||
>
|
||||
{keyToTitle(getStatus()).toLowerCase()}
|
||||
{getTitle()}
|
||||
</Badge>
|
||||
{/* {isCanceled && (
|
||||
<Badge variant="status" className="ml-2 bg-gray-500">
|
||||
|
||||
@@ -56,7 +56,7 @@ export const AttachModal = ({
|
||||
|
||||
if (entityId) {
|
||||
const entity = entities.find(
|
||||
(e: Entity) => e.id === entityId || e.internal_id === entityId,
|
||||
(e: Entity) => e.id === entityId || e.internal_id === entityId
|
||||
);
|
||||
const entityName = entity?.name || entity?.id || entity?.internal_id;
|
||||
return `${cusName} (${entityName})`;
|
||||
@@ -110,6 +110,7 @@ export const AttachModal = ({
|
||||
}
|
||||
|
||||
const dueToday = preview?.due_today;
|
||||
|
||||
if (dueToday && dueToday.total == 0) {
|
||||
return "Confirm";
|
||||
}
|
||||
@@ -169,7 +170,7 @@ export const AttachModal = ({
|
||||
navigateTo(
|
||||
`/integrations/stripe?redirect=${redirectUrl}`,
|
||||
navigation,
|
||||
env,
|
||||
env
|
||||
);
|
||||
} else {
|
||||
toast.error(getBackendErr(error, "Error creating product"));
|
||||
@@ -228,7 +229,7 @@ export const AttachModal = ({
|
||||
<DialogFooter
|
||||
className={cn(
|
||||
"bg-stone-100 flex items-center h-10 gap-0 border-t border-zinc-200",
|
||||
mainWidth,
|
||||
mainWidth
|
||||
)}
|
||||
>
|
||||
{invoiceAllowed() && (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ProductItem,
|
||||
ProductItemFeatureType,
|
||||
} from "@autumn/shared";
|
||||
import { format } from "date-fns";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
export const AttachInfo = () => {
|
||||
@@ -44,13 +45,27 @@ export const AttachInfo = () => {
|
||||
);
|
||||
});
|
||||
|
||||
let text = `The customer is currently on ${currentProduct.name} v${currentProduct.version}. Switching to v${product.version} will update the customer's features immediately, and from ${formatUnixToDate(preview.due_next_cycle.due_at)} onwards they will pay any new prices`;
|
||||
return (
|
||||
<>
|
||||
<span>
|
||||
You are switching this customer to version {product.version} of{" "}
|
||||
{product.name}. Their features will update immediately and from{" "}
|
||||
{format(preview.due_next_cycle.due_at, "d MMM")} onwards, they will
|
||||
pay any new prices
|
||||
{usagePriceExists ? " (including usage from the last cycle)" : ""}.
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (usagePriceExists) {
|
||||
text += ` (including usage from the last cycle).`;
|
||||
} else {
|
||||
text += `.`;
|
||||
}
|
||||
const text = `You are switching this customer to version ${product.version} of ${product.name}.`;
|
||||
|
||||
// let text = `The customer is currently on ${currentProduct.name} v${currentProduct.version}. Switching to v${product.version} will update the customer's features immediately, and from ${formatUnixToDate(preview.due_next_cycle.due_at)} onwards they will pay any new prices`;
|
||||
|
||||
// if (usagePriceExists) {
|
||||
// text += ` (including usage from the last cycle).`;
|
||||
// } else {
|
||||
// text += `.`;
|
||||
// }
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ const CreateAPIKey = () => {
|
||||
}, [copied]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
console.log("creating api key", apiKeyName ? apiKeyName : name);
|
||||
setLoading(true);
|
||||
try {
|
||||
const { api_key } = await DevService.createAPIKey(axiosInstance, {
|
||||
|
||||
Reference in New Issue
Block a user