feat: invoice checkout mode
This commit is contained in:
@@ -4,19 +4,19 @@
|
||||
source "$(dirname "$0")/config.sh"
|
||||
|
||||
# If contains setup then run $MOCHA_SETUP
|
||||
# if [[ "$2" == *"setup"* ]]; then
|
||||
# MOCHA_PARALLEL=true $MOCHA_SETUP
|
||||
# fi
|
||||
if [[ "$1" == *"setup"* ]]; then
|
||||
MOCHA_PARALLEL=true $MOCHA_SETUP
|
||||
fi
|
||||
|
||||
$MOCHA_CMD \
|
||||
'tests/attach/basic/*.ts' \
|
||||
'tests/attach/upgrade/*.ts' \
|
||||
'tests/attach/downgrade/*.ts' \
|
||||
'tests/attach/checkout/*.ts'
|
||||
|
||||
$MOCHA_CMD \
|
||||
'tests/attach/entities/*.ts' \
|
||||
'tests/attach/free/*.ts'\
|
||||
'tests/attach/addOn/*.ts'
|
||||
|
||||
$MOCHA_CMD \
|
||||
'tests/attach/checkout/*.ts' \
|
||||
'tests/attach/entities/*.ts' \
|
||||
'tests/attach/free/*.ts'\
|
||||
|
||||
# 'tests/attach/basic/basic2.ts' \
|
||||
@@ -17,15 +17,15 @@ $MOCHA_CMD 'tests/contUse/track/*.ts'
|
||||
|
||||
$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'
|
||||
# # 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/attach/multiProduct/*.ts' \
|
||||
# 'tests/advanced/usageLimit/*.ts'
|
||||
|
||||
$MOCHA_CMD 'tests/advanced/usage/*.ts'
|
||||
# $MOCHA_CMD 'tests/advanced/usage/*.ts'
|
||||
4
server/src/external/autumn/autumnCli.ts
vendored
4
server/src/external/autumn/autumnCli.ts
vendored
@@ -170,7 +170,9 @@ export class AutumnInt {
|
||||
|
||||
return data;
|
||||
}
|
||||
async checkout(params: CheckoutParams) {
|
||||
async checkout(
|
||||
params: CheckoutParams & { invoice?: boolean; force_checkout?: boolean }
|
||||
) {
|
||||
// const data = await this.post(`/attach`, {
|
||||
// customer_id: customerId,
|
||||
// product_id: productId,
|
||||
|
||||
@@ -42,6 +42,7 @@ export const createWebhookEndpoint = async (
|
||||
"invoice.upcoming",
|
||||
"invoice.created",
|
||||
"invoice.finalized",
|
||||
"invoice.updated",
|
||||
"subscription_schedule.canceled",
|
||||
"customer.discount.deleted",
|
||||
],
|
||||
|
||||
@@ -92,11 +92,16 @@ export const createStripeSub = async ({
|
||||
: undefined,
|
||||
|
||||
coupon: reward ? reward.id : undefined,
|
||||
expand: ["latest_invoice"],
|
||||
});
|
||||
|
||||
if (invoiceOnly && finalizeInvoice) {
|
||||
await stripeCli.invoices.finalizeInvoice(
|
||||
subscription.latest_invoice as string
|
||||
if (
|
||||
invoiceOnly &&
|
||||
finalizeInvoice &&
|
||||
(subscription.latest_invoice as Stripe.Invoice).status === "draft"
|
||||
) {
|
||||
subscription.latest_invoice = await stripeCli.invoices.finalizeInvoice(
|
||||
(subscription.latest_invoice as Stripe.Invoice).id
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
10
server/src/external/stripe/stripeWebhooks.ts
vendored
10
server/src/external/stripe/stripeWebhooks.ts
vendored
@@ -21,6 +21,7 @@ import { createStripeCli } from "./utils.js";
|
||||
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { handleInvoiceUpdated } from "./webhookHandlers/handleInvoiceUpdated.js";
|
||||
|
||||
export const stripeWebhookRouter: Router = express.Router();
|
||||
|
||||
@@ -183,6 +184,15 @@ stripeWebhookRouter.post(
|
||||
});
|
||||
break;
|
||||
|
||||
case "invoice.updated":
|
||||
await handleInvoiceUpdated({
|
||||
stripeCli,
|
||||
env,
|
||||
event,
|
||||
req: request,
|
||||
});
|
||||
break;
|
||||
|
||||
case "invoice.created":
|
||||
const createdInvoice = event.data.object;
|
||||
await handleInvoiceCreated({
|
||||
|
||||
@@ -101,7 +101,8 @@ export const handleRemainingSets = async ({
|
||||
})) as Stripe.Subscription;
|
||||
|
||||
subs.push(subscription);
|
||||
invoiceIds.push(subscription.latest_invoice as string);
|
||||
const latestInvoice = subscription.latest_invoice as Stripe.Invoice;
|
||||
invoiceIds.push(latestInvoice.id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
|
||||
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
|
||||
import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js";
|
||||
import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js";
|
||||
|
||||
const handleOneOffInvoicePaid = async ({
|
||||
db,
|
||||
@@ -92,12 +93,12 @@ const convertToChargeAutomatically = async ({
|
||||
logger.info(`Converting to charge automatically`);
|
||||
// 1. Get payment intent
|
||||
const paymentIntent = await stripeCli.paymentIntents.retrieve(
|
||||
invoice.payment_intent as string,
|
||||
invoice.payment_intent as string
|
||||
);
|
||||
|
||||
// 2. Get payment method
|
||||
const paymentMethod = await stripeCli.paymentMethods.retrieve(
|
||||
paymentIntent.payment_method as string,
|
||||
paymentIntent.payment_method as string
|
||||
);
|
||||
|
||||
await stripeCli.paymentMethods.attach(paymentMethod.id, {
|
||||
@@ -113,7 +114,7 @@ const convertToChargeAutomatically = async ({
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Convert to charge automatically: error updating subscription ${sub.id}`,
|
||||
`Convert to charge automatically: error updating subscription ${sub.id}`
|
||||
);
|
||||
logger.warn(error);
|
||||
}
|
||||
@@ -153,6 +154,17 @@ export const handleInvoicePaid = async ({
|
||||
stripeId: invoiceData.id,
|
||||
});
|
||||
|
||||
if (invoice.metadata?.autumn_metadata_id) {
|
||||
await handleInvoiceCheckoutPaid({
|
||||
req,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
stripeCli,
|
||||
invoice,
|
||||
});
|
||||
}
|
||||
|
||||
await handleInvoicePaidDiscount({
|
||||
db,
|
||||
expandedInvoice: invoice,
|
||||
@@ -174,7 +186,7 @@ export const handleInvoicePaid = async ({
|
||||
// TODO: Send alert
|
||||
if (invoice.livemode) {
|
||||
logger.warn(
|
||||
`invoice.paid: customer product not found for invoice ${invoice.id}`,
|
||||
`invoice.paid: customer product not found for invoice ${invoice.id}`
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -199,7 +211,7 @@ export const handleInvoicePaid = async ({
|
||||
let invoiceItems = await getInvoiceItems({
|
||||
stripeInvoice: invoice,
|
||||
prices: activeCusProducts.flatMap((p) =>
|
||||
p.customer_prices.map((cpr: FullCustomerPrice) => cpr.price),
|
||||
p.customer_prices.map((cpr: FullCustomerPrice) => cpr.price)
|
||||
),
|
||||
logger,
|
||||
});
|
||||
|
||||
39
server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts
vendored
Normal file
39
server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import { AppEnv, InvoiceStatus } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import { getFullStripeInvoice } from "../stripeInvoiceUtils.js";
|
||||
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
|
||||
|
||||
export const handleInvoiceUpdated = async ({
|
||||
env,
|
||||
event,
|
||||
stripeCli,
|
||||
req,
|
||||
}: {
|
||||
env: AppEnv;
|
||||
event: Stripe.Event;
|
||||
stripeCli: Stripe;
|
||||
req: any;
|
||||
}) => {
|
||||
const invoiceObject = event.data.object as Stripe.Invoice;
|
||||
const invoice = await getFullStripeInvoice({
|
||||
stripeCli,
|
||||
stripeId: invoiceObject.id,
|
||||
});
|
||||
|
||||
const prevAttributes = event.data.previous_attributes as any;
|
||||
const invoiceVoided =
|
||||
prevAttributes?.status !== "void" && invoice.status === "void";
|
||||
|
||||
const { logger } = req;
|
||||
|
||||
if (invoiceVoided) {
|
||||
logger.info(`Invoice has been voided!`);
|
||||
await InvoiceService.updateByStripeId({
|
||||
db: req.db,
|
||||
stripeId: invoiceObject.id,
|
||||
updates: {
|
||||
status: InvoiceStatus.Void,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -202,6 +202,12 @@ export const handleUsageEvent = async ({
|
||||
entityId: entity_id,
|
||||
};
|
||||
|
||||
// console.log("Customer:", customer);
|
||||
// console.log(
|
||||
// "Is paid continuous use:",
|
||||
// isPaidContinuousUse({ feature, fullCus: customer })
|
||||
// );
|
||||
|
||||
if (isPaidContinuousUse({ feature, fullCus: customer })) {
|
||||
console.log(`Running update usage task synchronously`);
|
||||
await runUpdateUsageTask({
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
AttachParams,
|
||||
AttachResultSchema,
|
||||
} from "../cusProducts/AttachParams.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
import { isOneOff } from "@/internal/products/productUtils.js";
|
||||
import { attachParamsToProduct } from "../attach/attachUtils/convertAttachParams.js";
|
||||
import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js";
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachConfig,
|
||||
ProrationBehavior,
|
||||
SuccessCode,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import { handleOneOffFunction } from "../attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
|
||||
|
||||
export const handleCreateInvoiceCheckout = async ({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
config,
|
||||
}: {
|
||||
req: any;
|
||||
res?: any;
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
}) => {
|
||||
// if one off
|
||||
const { stripeCli } = attachParams;
|
||||
|
||||
let invoiceResult;
|
||||
if (isOneOff(attachParams.prices)) {
|
||||
invoiceResult = await handleOneOffFunction({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
} else {
|
||||
invoiceResult = await handlePaidProduct({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
const { invoices, anchorToUnix, subs }: any = invoiceResult;
|
||||
|
||||
const metadataId = await createCheckoutMetadata({
|
||||
db: req.db,
|
||||
attachParams: {
|
||||
...attachParams,
|
||||
anchorToUnix,
|
||||
subIds: subs.map((s: Stripe.Subscription) => s.id),
|
||||
config,
|
||||
} as any,
|
||||
});
|
||||
|
||||
for (const invoice of invoices) {
|
||||
await stripeCli.invoices.update(invoice.id, {
|
||||
metadata: {
|
||||
autumn_metadata_id: metadataId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// AttachResultSchema.parse({
|
||||
// checkout_url: checkout.url,
|
||||
// code: SuccessCode.CheckoutCreated,
|
||||
// message: `Successfully created checkout for customer ${
|
||||
// customer.id || customer.internal_id
|
||||
// }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
|
||||
// product_ids: attachParams.products.map((p) => p.id),
|
||||
// customer_id: customer.id || customer.internal_id,
|
||||
// });
|
||||
if (res) {
|
||||
res.status(200).json(
|
||||
AttachResultSchema.parse({
|
||||
checkout_url: invoices[0].hosted_invoice_url,
|
||||
code: SuccessCode.CheckoutCreated,
|
||||
message: `Successfully created invoice checkout for customer ${
|
||||
attachParams.customer.id || attachParams.customer.internal_id
|
||||
}, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
|
||||
product_ids: attachParams.products.map((p) => p.id),
|
||||
customer_id:
|
||||
attachParams.customer.id || attachParams.customer.internal_id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { invoices };
|
||||
};
|
||||
@@ -106,7 +106,7 @@ export const handleOneOffFunction = async ({
|
||||
|
||||
// Create invoice
|
||||
logger.info("1. Creating invoice");
|
||||
const stripeInvoice = await stripeCli.invoices.create({
|
||||
let stripeInvoice = await stripeCli.invoices.create({
|
||||
customer: customer.processor.id!,
|
||||
auto_advance: false,
|
||||
currency: org.default_currency!,
|
||||
@@ -117,6 +117,8 @@ export const handleOneOffFunction = async ({
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
collection_method: attachParams.invoiceOnly ? "send_invoice" : undefined,
|
||||
days_until_due: attachParams.invoiceOnly ? 30 : undefined,
|
||||
});
|
||||
|
||||
logger.info("2. Creating invoice items");
|
||||
@@ -128,6 +130,23 @@ export const handleOneOffFunction = async ({
|
||||
} as any);
|
||||
}
|
||||
|
||||
if (config.invoiceCheckout) {
|
||||
if (stripeInvoice.status === "draft") {
|
||||
stripeInvoice = await stripeCli.invoices.finalizeInvoice(
|
||||
stripeInvoice.id
|
||||
);
|
||||
}
|
||||
|
||||
await insertInvoiceFromAttach({
|
||||
db: req.db,
|
||||
attachParams,
|
||||
invoiceId: stripeInvoice.id,
|
||||
logger,
|
||||
});
|
||||
|
||||
return { invoices: [stripeInvoice], subs: [], anchorToUnix: undefined };
|
||||
}
|
||||
|
||||
// Create invoice items
|
||||
if (!invoiceOnly) {
|
||||
await stripeCli.invoices.finalizeInvoice(stripeInvoice.id);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||
@@ -14,8 +16,6 @@ import {
|
||||
} from "@/internal/invoices/invoiceUtils.js";
|
||||
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { formatUnixToDateTime } from "@/utils/genUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
APIVersion,
|
||||
@@ -122,7 +122,7 @@ export const handlePaidProduct = async ({
|
||||
freeTrial,
|
||||
invoiceOnly,
|
||||
itemSet,
|
||||
finalizeInvoice: attachParams.finalizeInvoice,
|
||||
finalizeInvoice: config.invoiceCheckout,
|
||||
anchorToUnix: billingCycleAnchorUnix,
|
||||
reward: i == 0 ? reward : undefined,
|
||||
now: attachParams.now,
|
||||
@@ -148,9 +148,6 @@ export const handlePaidProduct = async ({
|
||||
}
|
||||
}
|
||||
|
||||
// Add product and entitlements to customer
|
||||
const batchInsert = [];
|
||||
|
||||
const anchorToUnix =
|
||||
subscriptions.length > 0
|
||||
? subscriptions[0].current_period_end * 1000
|
||||
@@ -158,6 +155,32 @@ export const handlePaidProduct = async ({
|
||||
? mergeSubs[0].current_period_end * 1000
|
||||
: undefined;
|
||||
|
||||
const batchInsertInvoice: any = [];
|
||||
for (const sub of subscriptions) {
|
||||
batchInsertInvoice.push(
|
||||
insertInvoiceFromAttach({
|
||||
db: req.db,
|
||||
// invoiceId: sub.latest_invoice as string,
|
||||
stripeInvoice: sub.latest_invoice as Stripe.Invoice,
|
||||
attachParams,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
}
|
||||
const invoices = await Promise.all(batchInsertInvoice);
|
||||
|
||||
if (config.invoiceCheckout) {
|
||||
return {
|
||||
invoices: subscriptions.map((s) => s.latest_invoice as Stripe.Invoice),
|
||||
subs: subscriptions,
|
||||
anchorToUnix,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
// Add product and entitlements to customer
|
||||
const batchInsert = [];
|
||||
|
||||
for (const product of products) {
|
||||
batchInsert.push(
|
||||
createFullCusProduct({
|
||||
@@ -173,19 +196,6 @@ export const handlePaidProduct = async ({
|
||||
}
|
||||
await Promise.all(batchInsert);
|
||||
|
||||
const batchInsertInvoice: any = [];
|
||||
for (const sub of subscriptions) {
|
||||
batchInsertInvoice.push(
|
||||
insertInvoiceFromAttach({
|
||||
db: req.db,
|
||||
invoiceId: sub.latest_invoice as string,
|
||||
attachParams,
|
||||
logger,
|
||||
})
|
||||
);
|
||||
}
|
||||
const invoices = await Promise.all(batchInsertInvoice);
|
||||
|
||||
if (res) {
|
||||
let apiVersion = attachParams.apiVersion || APIVersion.v1;
|
||||
const productNames = products.map((p) => p.name).join(", ");
|
||||
|
||||
@@ -8,10 +8,14 @@ export const addSubItemsToRemove = async ({
|
||||
cusProduct,
|
||||
itemSet,
|
||||
}: {
|
||||
sub: Stripe.Subscription;
|
||||
sub?: Stripe.Subscription | null;
|
||||
cusProduct: FullCusProduct;
|
||||
itemSet: ItemSet;
|
||||
}) => {
|
||||
if (!sub) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of sub.items.data) {
|
||||
let shouldRemove = subItemInCusProduct({
|
||||
cusProduct,
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { MetadataService } from "@/internal/metadata/MetadataService.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AppEnv, AttachScenario, Organization } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const handleInvoiceCheckoutPaid = async ({
|
||||
req,
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
stripeCli,
|
||||
invoice,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
db: DrizzleCli;
|
||||
stripeCli: Stripe;
|
||||
invoice: Stripe.Invoice;
|
||||
}) => {
|
||||
const metadataId = invoice.metadata?.autumn_metadata_id!;
|
||||
|
||||
const metadata = await MetadataService.get({
|
||||
db,
|
||||
id: metadataId,
|
||||
});
|
||||
|
||||
const { subIds, anchorToUnix, config, ...rest } = metadata?.data;
|
||||
const attachParams = rest as AttachParams;
|
||||
|
||||
if (!attachParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reqMatch =
|
||||
attachParams.org.id === org.id && attachParams.customer.env === env;
|
||||
|
||||
if (!reqMatch) return;
|
||||
|
||||
const batchInsert = [];
|
||||
for (const product of attachParams.products) {
|
||||
batchInsert.push(
|
||||
createFullCusProduct({
|
||||
db,
|
||||
attachParams: attachToInsertParams(attachParams, product),
|
||||
subscriptionIds: subIds,
|
||||
anchorToUnix,
|
||||
carryExistingUsages: config.carryUsage,
|
||||
scenario: AttachScenario.New,
|
||||
logger: req.logger,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batchInsert);
|
||||
|
||||
req.logger.info(
|
||||
`✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
insertInvoiceFromAttach,
|
||||
} from "@/internal/invoices/invoiceUtils.js";
|
||||
import { updateSubsDiffInt } from "./updateSubsDiffInt.js";
|
||||
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
|
||||
|
||||
export const handleUpgradeDiffInterval = async ({
|
||||
req,
|
||||
@@ -41,12 +42,12 @@ export const handleUpgradeDiffInterval = async ({
|
||||
|
||||
const product = products[0];
|
||||
|
||||
let { curMainProduct: curCusProduct } = getExistingCusProducts({
|
||||
product,
|
||||
cusProducts: cusProducts || [],
|
||||
internalEntityId: attachParams.internalEntityId,
|
||||
});
|
||||
|
||||
// let { curMainProduct: curCusProduct } = getExistingCusProducts({
|
||||
// product,
|
||||
// cusProducts: cusProducts || [],
|
||||
// internalEntityId: attachParams.internalEntityId,
|
||||
// });
|
||||
let curCusProduct = attachParamsToCurCusProduct({ attachParams });
|
||||
curCusProduct = curCusProduct!;
|
||||
|
||||
const stripeSubs = await getStripeSubs({
|
||||
|
||||
@@ -39,8 +39,8 @@ export const updateSubsDiffInt = async ({
|
||||
carryExistingUsages: config.carryUsage,
|
||||
});
|
||||
|
||||
const firstSub = stripeSubs[0];
|
||||
const firstItemSet = itemSets[0];
|
||||
const firstSub = stripeSubs?.[0];
|
||||
const firstItemSet = itemSets?.[0];
|
||||
|
||||
await addSubItemsToRemove({
|
||||
sub: firstSub,
|
||||
@@ -127,7 +127,8 @@ export const updateSubsDiffInt = async ({
|
||||
});
|
||||
|
||||
newSubs.push(newSub);
|
||||
newInvoiceIds.push(newSub.latest_invoice as string);
|
||||
const latestInvoice = newSub.latest_invoice as Stripe.Invoice;
|
||||
newInvoiceIds.push(latestInvoice.id);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -39,25 +39,11 @@ export const updateSubsByInt = async ({
|
||||
|
||||
attachParams.replaceables = replaceables;
|
||||
|
||||
// logger.info(`Cont use items`);
|
||||
// logger.info(
|
||||
// `New items: `,
|
||||
// newItems.map(
|
||||
// (item) => `${item.description} | Amount: ${item.amount || item.price}`,
|
||||
// ),
|
||||
// );
|
||||
// logger.info(
|
||||
// "Replaceables: ",
|
||||
// replaceables.map((r) => `${r.ent.feature_id}`),
|
||||
// );
|
||||
|
||||
const itemSets = await getStripeSubItems({ attachParams });
|
||||
|
||||
const invoices: Stripe.Invoice[] = [];
|
||||
|
||||
// const replaceables: Replaceable[] = [];
|
||||
for (const sub of stripeSubs) {
|
||||
// let interval = subToAutumnInterval(sub);
|
||||
|
||||
let subInterval = subToAutumnInterval(sub);
|
||||
let itemSet = itemSets.find((itemSet) => {
|
||||
return intervalsSame({
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { APIVersion, BillingType, FullCusProduct } from "@autumn/shared";
|
||||
import {
|
||||
APIVersion,
|
||||
AttachConfig,
|
||||
BillingType,
|
||||
FullCusProduct,
|
||||
} from "@autumn/shared";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
|
||||
@@ -22,6 +27,7 @@ import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice
|
||||
import {
|
||||
notNullish,
|
||||
notNullOrUndefined,
|
||||
nullish,
|
||||
nullOrUndefined,
|
||||
} from "@/utils/genUtils.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
@@ -35,14 +41,17 @@ import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.j
|
||||
import { handleAttach } from "./handleAttach.js";
|
||||
import { handleCheckout } from "./checkout/handleCheckout.js";
|
||||
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { attachParamsToCurCusProduct } from "./attachUtils/convertAttachParams.js";
|
||||
|
||||
export const attachRouter: Router = Router();
|
||||
|
||||
export const handlePrepaidErrors = async ({
|
||||
attachParams,
|
||||
config,
|
||||
useCheckout = false,
|
||||
}: {
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
useCheckout?: boolean;
|
||||
}) => {
|
||||
const { prices, entitlements, optionsList } = attachParams;
|
||||
@@ -57,7 +66,8 @@ export const handlePrepaidErrors = async ({
|
||||
let options = getEntOptions(optionsList, priceEnt);
|
||||
|
||||
// 1. If not checkout, quantity should be defined
|
||||
if (!useCheckout && nullOrUndefined(options?.quantity)) {
|
||||
const regularCheckout = useCheckout && !config.invoiceCheckout;
|
||||
if (!regularCheckout && nullOrUndefined(options?.quantity)) {
|
||||
throw new RecaseError({
|
||||
message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
|
||||
code: ErrCode.InvalidOptions,
|
||||
@@ -161,6 +171,7 @@ export const checkStripeConnections = async ({
|
||||
}
|
||||
|
||||
const batchProductUpdates = [];
|
||||
|
||||
if (createCus) {
|
||||
batchProductUpdates.push(
|
||||
createStripeCusIfNotExists({
|
||||
@@ -172,6 +183,7 @@ export const checkStripeConnections = async ({
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const product of products) {
|
||||
batchProductUpdates.push(
|
||||
checkStripeProductExists({
|
||||
@@ -207,6 +219,9 @@ export const createStripePrices = async ({
|
||||
const { prices, entitlements, products, org, stripeCli } = attachParams;
|
||||
|
||||
const batchPriceUpdates = [];
|
||||
|
||||
// const curCusProduct = attachParamsToCurCusProduct({ attachParams });
|
||||
|
||||
for (const price of prices) {
|
||||
let product = getProductForPrice(price, products);
|
||||
|
||||
|
||||
@@ -77,13 +77,6 @@ export const checkToAttachParams = async ({
|
||||
|
||||
// Others
|
||||
apiVersion,
|
||||
// successUrl: attachBody.success_url,
|
||||
// invoiceOnly: attachBody.invoice_only,
|
||||
// billingAnchor: attachBody.billing_cycle_anchor,
|
||||
// metadata: attachBody.metadata,
|
||||
// disableFreeTrial: attachBody.free_trial === false || false,
|
||||
// checkoutSessionParams: attachBody.checkout_session_params,
|
||||
// isCustom: attachBody.is_custom,
|
||||
};
|
||||
|
||||
return attachParams;
|
||||
|
||||
@@ -66,8 +66,9 @@ export const getAttachParams = async ({
|
||||
// Others
|
||||
apiVersion,
|
||||
successUrl: attachBody.success_url,
|
||||
invoiceOnly: attachBody.invoice_only || attachBody.invoice,
|
||||
finalizeInvoice: attachBody.invoice,
|
||||
invoiceOnly: attachBody.invoice,
|
||||
// || attachBody.invoice_only
|
||||
|
||||
billingAnchor: attachBody.billing_cycle_anchor,
|
||||
metadata: attachBody.metadata,
|
||||
disableFreeTrial: attachBody.free_trial === false || false,
|
||||
|
||||
@@ -10,7 +10,7 @@ export const attachParamsToCurCusProduct = ({
|
||||
const { curMainProduct, curSameProduct, curScheduledProduct } =
|
||||
attachParamToCusProducts({ attachParams });
|
||||
|
||||
return curMainProduct || curSameProduct;
|
||||
return curSameProduct || curMainProduct;
|
||||
};
|
||||
|
||||
export const attachParamToCusProducts = ({
|
||||
|
||||
@@ -135,9 +135,6 @@ const checkSameCustom = async ({
|
||||
features: attachParams.features,
|
||||
});
|
||||
|
||||
// console.log("Attach params free trial:", attachParams.freeTrial);
|
||||
// console.log("Cur same product free trial:", curSameProduct.free_trial);
|
||||
|
||||
if (itemsSame && freeTrialsSame) {
|
||||
throw new RecaseError({
|
||||
message: `Items specified for ${product.name} are the same as the existing product, can't attach again`,
|
||||
@@ -145,6 +142,11 @@ const checkSameCustom = async ({
|
||||
});
|
||||
}
|
||||
|
||||
// const curPrices = cusProductToPrices({ cusProduct: curSameProduct });
|
||||
// if (isFreeProduct(curPrices) && !isFreeProduct(attachParams.prices)) {
|
||||
// return AttachBranch.MainIsFree;
|
||||
// }
|
||||
|
||||
if (onlyEntsChanged) {
|
||||
return AttachBranch.SameCustomEnts;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const intervalsAreSame = ({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
let curCusProduct = curMainProduct || curSameProduct;
|
||||
let curCusProduct = curSameProduct || curMainProduct;
|
||||
|
||||
if (!curCusProduct) {
|
||||
return false;
|
||||
@@ -143,8 +143,17 @@ export const getAttachConfig = async ({
|
||||
branch == AttachBranch.MainIsTrial ||
|
||||
org.config.merge_billing_cycles === false;
|
||||
|
||||
const invoiceAndEnable =
|
||||
attachParams.invoiceOnly && attachBody.enable_product_immediately;
|
||||
|
||||
const invoiceCheckout =
|
||||
attachParams.invoiceOnly === true && !attachBody.enable_product_immediately;
|
||||
|
||||
const checkoutFlow =
|
||||
isPublic || forceCheckout || (noPaymentMethod && !invoiceOnly);
|
||||
isPublic ||
|
||||
forceCheckout ||
|
||||
invoiceCheckout ||
|
||||
(noPaymentMethod && !invoiceAndEnable);
|
||||
|
||||
const onlyCheckout = !isFree && checkoutFlow;
|
||||
|
||||
@@ -155,6 +164,7 @@ export const getAttachConfig = async ({
|
||||
proration,
|
||||
disableTrial,
|
||||
invoiceOnly: flags.invoiceOnly,
|
||||
invoiceCheckout,
|
||||
disableMerge,
|
||||
sameIntervals,
|
||||
carryTrial,
|
||||
@@ -174,6 +184,7 @@ export const getDefaultAttachConfig = () => {
|
||||
disableMerge: false,
|
||||
sameIntervals: false,
|
||||
carryTrial: false,
|
||||
invoiceCheckout: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
|
||||
@@ -25,6 +25,7 @@ import { deleteCurrentScheduledProduct } from "./deleteCurrentScheduledProduct.j
|
||||
import { handleOneOffFunction } from "../attachFunctions/addProductFlow/handleOneOffFunction.js";
|
||||
import { handleUpgradeSameInterval } from "../attachFunctions/upgradeSameIntFlow/handleUpgradeSameInt.js";
|
||||
import { CusProductService } from "../../cusProducts/CusProductService.js";
|
||||
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
|
||||
|
||||
/*
|
||||
1. If from new version, free trial should just carry over
|
||||
@@ -217,6 +218,14 @@ export const runAttachFunction = async ({
|
||||
}
|
||||
|
||||
if (attachFunction == AttachFunction.CreateCheckout) {
|
||||
if (config.invoiceCheckout) {
|
||||
return await handleCreateInvoiceCheckout({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
}
|
||||
return await handleCreateCheckout({
|
||||
req,
|
||||
res,
|
||||
|
||||
@@ -2,9 +2,15 @@ import RecaseError from "@/utils/errorUtils.js";
|
||||
import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { AttachBranch, AttachErrCode, UsagePriceConfig } from "@autumn/shared";
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachConfig,
|
||||
AttachErrCode,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { AttachBody } from "@autumn/shared";
|
||||
import { AttachConfig, AttachFlags } from "../models/AttachFlags.js";
|
||||
import { AttachFlags } from "../models/AttachFlags.js";
|
||||
|
||||
import {
|
||||
getEntOptions,
|
||||
priceIsOneOffAndTiered,
|
||||
@@ -25,8 +31,10 @@ import { Decimal } from "decimal.js";
|
||||
const handleNonCheckoutErrors = ({
|
||||
flags,
|
||||
action,
|
||||
config,
|
||||
}: {
|
||||
flags: AttachFlags;
|
||||
config: AttachConfig;
|
||||
action: string;
|
||||
}) => {
|
||||
const { isPublic, forceCheckout, noPaymentMethod } = flags;
|
||||
@@ -47,14 +55,22 @@ const handleNonCheckoutErrors = ({
|
||||
message: `Not allowed to ${action} because customer has no payment method on file`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
} else if (config.invoiceCheckout) {
|
||||
throw new RecaseError({
|
||||
message: `Not allowed to ${action} when using 'invoice': true`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrepaidErrors = async ({
|
||||
attachParams,
|
||||
config,
|
||||
useCheckout = false,
|
||||
}: {
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
useCheckout?: boolean;
|
||||
}) => {
|
||||
const { prices, entitlements, optionsList } = attachParams;
|
||||
@@ -69,7 +85,10 @@ const handlePrepaidErrors = async ({
|
||||
let options = getEntOptions(optionsList, priceEnt);
|
||||
|
||||
// 1. If not checkout, quantity should be defined
|
||||
if (!useCheckout && nullOrUndefined(options?.quantity)) {
|
||||
|
||||
const regularCheckout = useCheckout && !config.invoiceCheckout;
|
||||
|
||||
if (!regularCheckout && nullOrUndefined(options?.quantity)) {
|
||||
throw new RecaseError({
|
||||
message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
|
||||
code: ErrCode.InvalidOptions,
|
||||
@@ -197,6 +216,8 @@ export const handleAttachErrors = async ({
|
||||
}) => {
|
||||
const { onlyCheckout } = config;
|
||||
|
||||
// Invoice no payment enabled: onlyCheckout
|
||||
|
||||
if (onlyCheckout || flags.isPublic) {
|
||||
let upgradeDowngradeFlows = [
|
||||
AttachBranch.Upgrade,
|
||||
@@ -206,6 +227,7 @@ export const handleAttachErrors = async ({
|
||||
if (upgradeDowngradeFlows.includes(branch)) {
|
||||
handleNonCheckoutErrors({
|
||||
flags,
|
||||
config,
|
||||
action: "perform upgrade or downgrade",
|
||||
});
|
||||
}
|
||||
@@ -218,6 +240,7 @@ export const handleAttachErrors = async ({
|
||||
handleNonCheckoutErrors({
|
||||
flags,
|
||||
action: "update current product",
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -236,6 +259,7 @@ export const handleAttachErrors = async ({
|
||||
|
||||
await handlePrepaidErrors({
|
||||
attachParams,
|
||||
config,
|
||||
useCheckout: onlyCheckout,
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
|
||||
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
|
||||
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
|
||||
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
|
||||
import { checkStripeConnections } from "../attachRouter.js";
|
||||
import {
|
||||
checkStripeConnections,
|
||||
handlePrepaidErrors,
|
||||
} from "../attachRouter.js";
|
||||
import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js";
|
||||
import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
|
||||
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||
@@ -24,6 +27,7 @@ import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePrice
|
||||
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||
import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { getHasProrations } from "./getHasProrations.js";
|
||||
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
|
||||
|
||||
const getAttachVars = async ({
|
||||
req,
|
||||
@@ -108,12 +112,19 @@ export const handleCheckout = (req: any, res: any) =>
|
||||
const { logger, features } = req;
|
||||
const attachBody = AttachBodySchema.parse(req.body);
|
||||
|
||||
const { attachParams, branch, func } = await getAttachVars({
|
||||
const { attachParams, branch, func, config } = await getAttachVars({
|
||||
req,
|
||||
attachBody,
|
||||
});
|
||||
|
||||
let checkoutUrl = null;
|
||||
|
||||
await handlePrepaidErrors({
|
||||
attachParams,
|
||||
config,
|
||||
useCheckout: config.onlyCheckout,
|
||||
});
|
||||
|
||||
if (func == AttachFunction.CreateCheckout) {
|
||||
await checkStripeConnections({
|
||||
req,
|
||||
@@ -122,31 +133,24 @@ export const handleCheckout = (req: any, res: any) =>
|
||||
useCheckout: true,
|
||||
});
|
||||
|
||||
const checkout = await handleCreateCheckout({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
returnCheckout: true,
|
||||
});
|
||||
if (config.invoiceCheckout) {
|
||||
const result = await handleCreateInvoiceCheckout({
|
||||
req,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
|
||||
checkoutUrl = checkout?.url;
|
||||
checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
|
||||
} else {
|
||||
const checkout = await handleCreateCheckout({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
returnCheckout: true,
|
||||
});
|
||||
|
||||
// const customer = attachParams.customer;
|
||||
// res.status(200).json(
|
||||
// CheckoutResponseSchema.parse({
|
||||
// url: checkout?.url,
|
||||
// customer_id: customer.id || customer.internal_id,
|
||||
// scenario: AttachScenario.New,
|
||||
// lines: [],
|
||||
// product: await getProductResponse({
|
||||
// product: attachParams.products[0],
|
||||
// features: features,
|
||||
// withDisplay: false,
|
||||
// options: attachParams.optionsList,
|
||||
// }),
|
||||
// })
|
||||
// );
|
||||
// return;
|
||||
checkoutUrl = checkout?.url;
|
||||
}
|
||||
}
|
||||
|
||||
await getCheckoutOptions({
|
||||
|
||||
@@ -30,6 +30,9 @@ export const handleAttachPreview = (req: any, res: any) =>
|
||||
logger,
|
||||
});
|
||||
|
||||
console.log("Branch:", attachPreview.branch);
|
||||
console.log("Func:", attachPreview.func);
|
||||
|
||||
res.status(200).json(attachPreview);
|
||||
|
||||
return;
|
||||
|
||||
@@ -58,9 +58,7 @@ export const getCusWithCache = async ({
|
||||
|
||||
if (!skipCache && !skipGet) {
|
||||
try {
|
||||
const start = Date.now();
|
||||
const cached = await upstash!.get(cacheKey);
|
||||
const end = Date.now();
|
||||
if (cached) {
|
||||
return cached as FullCustomer;
|
||||
} else {
|
||||
@@ -88,8 +86,9 @@ export const getCusWithCache = async ({
|
||||
|
||||
if (!skipCache && notNullish(customer)) {
|
||||
try {
|
||||
await upstash!.set(cacheKey, customer);
|
||||
await upstash!.expire(cacheKey, 300); // Expire after 5 minutes...
|
||||
await upstash!.set(cacheKey, customer, {
|
||||
ex: 300,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to set cache: ${cacheKey}`, { error });
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import {
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { ACTIVE_STATUSES } from "../CusProductService.js";
|
||||
|
||||
export const cusProductsToCusPrices = ({
|
||||
cusProducts,
|
||||
inStatuses = [CusProductStatus.Active],
|
||||
inStatuses,
|
||||
billingType,
|
||||
}: {
|
||||
cusProducts: FullCusProduct[];
|
||||
@@ -29,7 +30,7 @@ export const cusProductsToCusPrices = ({
|
||||
const cusPrices: FullCustomerPrice[] = [];
|
||||
|
||||
for (const cusProduct of cusProducts) {
|
||||
if (!inStatuses.includes(cusProduct.status)) {
|
||||
if (inStatuses && !inStatuses.includes(cusProduct.status)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
cusProductsToCusPrices,
|
||||
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { getCusBalances } from "./getCusBalances.js";
|
||||
import { ACTIVE_STATUSES } from "../../cusProducts/CusProductService.js";
|
||||
|
||||
export const getCusFeaturesResponse = async ({
|
||||
cusProducts,
|
||||
@@ -26,7 +27,9 @@ export const getCusFeaturesResponse = async ({
|
||||
|
||||
const balances = await getCusBalances({
|
||||
cusEntsWithCusProduct: cusEnts,
|
||||
cusPrices: cusProductsToCusPrices({ cusProducts }),
|
||||
cusPrices: cusProductsToCusPrices({
|
||||
cusProducts,
|
||||
}),
|
||||
org,
|
||||
entity,
|
||||
apiVersion,
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
cusProductToPrices,
|
||||
} from "../customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { priceToFeature } from "../products/prices/priceUtils/convertPrice.js";
|
||||
import { ACTIVE_STATUSES } from "../customers/cusProducts/CusProductService.js";
|
||||
|
||||
export const validateFeatureId = (featureId: string) => {
|
||||
if (!featureId.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
@@ -227,7 +228,9 @@ export const isPaidContinuousUse = ({
|
||||
|
||||
let cusPrices = cusProductsToCusPrices({
|
||||
cusProducts: fullCus.customer_products,
|
||||
inStatuses: ACTIVE_STATUSES,
|
||||
});
|
||||
|
||||
let hasPaid = cusPrices.some((cp) => {
|
||||
let config = cp.price.config as UsagePriceConfig;
|
||||
if (config.internal_feature_id == feature.internal_id) {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const createCheckoutMetadata = async ({
|
||||
|
||||
export const getMetadataFromCheckoutSession = async (
|
||||
checkoutSession: Stripe.Checkout.Session,
|
||||
db: DrizzleCli,
|
||||
db: DrizzleCli
|
||||
) => {
|
||||
const metadataId = checkoutSession.metadata?.autumn_metadata_id;
|
||||
|
||||
|
||||
@@ -60,13 +60,6 @@ export const migrationToAttachParams = async ({
|
||||
|
||||
// Others
|
||||
apiVersion,
|
||||
// successUrl: attachBody.success_url,
|
||||
// invoiceOnly: attachBody.invoice_only,
|
||||
// billingAnchor: attachBody.billing_cycle_anchor,
|
||||
// metadata: attachBody.metadata,
|
||||
// disableFreeTrial: attachBody.free_trial === false || false,
|
||||
// checkoutSessionParams: attachBody.checkout_session_params,
|
||||
// isCustom: attachBody.is_custom,
|
||||
};
|
||||
|
||||
return attachParams;
|
||||
|
||||
@@ -696,6 +696,7 @@ export const runUpdateBalanceTask = async ({
|
||||
entityId,
|
||||
});
|
||||
|
||||
// console.time("refreshCusCache");
|
||||
await refreshCusCache({
|
||||
db,
|
||||
customerId,
|
||||
@@ -703,6 +704,7 @@ export const runUpdateBalanceTask = async ({
|
||||
env,
|
||||
entityId,
|
||||
});
|
||||
// console.timeEnd("refreshCusCache");
|
||||
|
||||
if (!cusEnts || cusEnts.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
|
||||
const clearCustomersInBatches = async ({
|
||||
export const clearCustomersInBatches = async ({
|
||||
db,
|
||||
org,
|
||||
batchSize = 450,
|
||||
@@ -38,6 +38,8 @@ const clearCustomersInBatches = async ({
|
||||
.map((c) => c.internalId)
|
||||
.filter((id) => id !== null);
|
||||
|
||||
console.log("Deleting customers:", customerIds);
|
||||
|
||||
await db
|
||||
.delete(customers)
|
||||
.where(inArray(customers.internal_id, customerIds));
|
||||
|
||||
@@ -45,7 +45,7 @@ const testCase = "addOn1";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing free add on, and updating free add on`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
@@ -112,7 +112,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing free add on, and updating f
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expect(customer.products.length).to.equal(2);
|
||||
expect(customer.products.length).to.equal(3);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: addOn,
|
||||
@@ -124,7 +124,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing free add on, and updating f
|
||||
});
|
||||
|
||||
const customItems = replaceItems({
|
||||
items: pro.items,
|
||||
items: addOn.items,
|
||||
featureId: TestFeature.Messages,
|
||||
newItem: constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
@@ -149,7 +149,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing free add on, and updating f
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expect(customer.products.length).to.equal(2);
|
||||
expect(customer.products.length).to.equal(3);
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: addOn,
|
||||
|
||||
118
server/tests/attach/checkout/checkout5.ts
Normal file
118
server/tests/attach/checkout/checkout5.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
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, createReward } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts } from "../utils.js";
|
||||
import {
|
||||
constructCoupon,
|
||||
constructProduct,
|
||||
} from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { expect } from "chai";
|
||||
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoiceCheckout.js";
|
||||
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
const testCase = "checkout5";
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout, no product till paid`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
|
||||
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({
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
// attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach pro product", async function () {
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
invoice: true,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
const invoice = customer.invoices?.[0];
|
||||
|
||||
expect(invoice).to.exist;
|
||||
expect(invoice.total).to.equal(getBasePrice({ product: pro }));
|
||||
expect(invoice.status).to.equal("open");
|
||||
|
||||
const product = customer.products.find((p) => p.id === pro.id);
|
||||
expect(product).to.not.exist;
|
||||
|
||||
await completeInvoiceCheckout({
|
||||
url: res.checkout_url,
|
||||
});
|
||||
|
||||
const customer2 = await autumn.customers.get(customerId);
|
||||
|
||||
const invoice2 = customer2.invoices?.[0];
|
||||
|
||||
expect(customer2.invoices.length).to.equal(1);
|
||||
expect(invoice2).to.exist;
|
||||
expect(invoice2.status).to.equal("paid");
|
||||
|
||||
expectProductAttached({
|
||||
customer: customer2,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
expectFeaturesCorrect({
|
||||
customer: customer2,
|
||||
product: pro,
|
||||
});
|
||||
});
|
||||
});
|
||||
158
server/tests/attach/checkout/checkout6.ts
Normal file
158
server/tests/attach/checkout/checkout6.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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 { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts } from "../utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoiceCheckout.js";
|
||||
import { expect } from "chai";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
|
||||
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
|
||||
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
export let premium = constructProduct({
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 250,
|
||||
}),
|
||||
],
|
||||
type: "premium",
|
||||
});
|
||||
|
||||
const testCase = "checkout6";
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout via checkout endpoint`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
|
||||
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, premium],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn,
|
||||
products: [pro, premium],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach pro product via invoice checkout", async function () {
|
||||
const res = await autumn.checkout({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
invoice: true,
|
||||
});
|
||||
|
||||
expect(res.url).to.exist;
|
||||
|
||||
await completeInvoiceCheckout({
|
||||
url: res.url!,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: pro,
|
||||
});
|
||||
|
||||
expectFeaturesCorrect({
|
||||
customer,
|
||||
product: pro,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have no URL returned if try to attach premium (with invoice true)", async function () {
|
||||
await expectAutumnError({
|
||||
func: async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
invoice: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const res = await autumn.checkout({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
invoice: true,
|
||||
});
|
||||
|
||||
expect(res.url).to.not.exist;
|
||||
});
|
||||
|
||||
it("should attach premium product via invoice enable immediately", async function () {
|
||||
const res = await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
invoice: true,
|
||||
enable_product_immediately: true,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: premium,
|
||||
});
|
||||
|
||||
expectFeaturesCorrect({
|
||||
customer,
|
||||
product: premium,
|
||||
});
|
||||
|
||||
const invoices = customer.invoices;
|
||||
expect(invoices.length).to.equal(2);
|
||||
expect(invoices[0].status).to.equal("draft");
|
||||
expect(invoices[0].total).to.equal(
|
||||
getBasePrice({ product: premium }) - getBasePrice({ product: pro })
|
||||
); // proration...
|
||||
});
|
||||
});
|
||||
141
server/tests/attach/checkout/checkout7.ts
Normal file
141
server/tests/attach/checkout/checkout7.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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 { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts } from "../utils.js";
|
||||
import {
|
||||
constructProduct,
|
||||
constructRawProduct,
|
||||
} from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import {
|
||||
constructFeatureItem,
|
||||
constructPrepaidItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoiceCheckout.js";
|
||||
import { expect } from "chai";
|
||||
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
export let addOn = constructRawProduct({
|
||||
id: "addOn",
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
billingUnits: 100,
|
||||
price: 10,
|
||||
isOneOff: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const testCase = "checkout7";
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout with one off product`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
|
||||
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, addOn],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn,
|
||||
products: [pro, addOn],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach pro product, then add on product via invoice checkout", async function () {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
const options = [
|
||||
{
|
||||
quantity: 200,
|
||||
feature_id: TestFeature.Messages,
|
||||
},
|
||||
];
|
||||
|
||||
const res2 = await autumn.checkout({
|
||||
customer_id: customerId,
|
||||
product_id: addOn.id,
|
||||
invoice: true,
|
||||
options,
|
||||
});
|
||||
|
||||
expect(res2.url).to.exist;
|
||||
|
||||
await completeInvoiceCheckout({
|
||||
url: res2.url!,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
customer,
|
||||
product: addOn,
|
||||
});
|
||||
|
||||
expectFeaturesCorrect({
|
||||
customer,
|
||||
product: addOn,
|
||||
otherProducts: [pro],
|
||||
options,
|
||||
});
|
||||
});
|
||||
|
||||
// it("should have no URL returned if try to attach add on (with invoice true)", async function () {
|
||||
// const res = await autumn.checkout({
|
||||
// customer_id: customerId,
|
||||
// product_id: addOn.id,
|
||||
// invoice: true,
|
||||
// });
|
||||
|
||||
// expect(res.url).to.not.exist;
|
||||
// });
|
||||
});
|
||||
@@ -12,12 +12,14 @@ const checkEntitledOnProduct = async ({
|
||||
totalAllowance,
|
||||
finish = false,
|
||||
usageBased = false,
|
||||
timeoutMs = 8000,
|
||||
}: {
|
||||
customerId: string;
|
||||
product: any;
|
||||
totalAllowance?: number;
|
||||
finish?: boolean;
|
||||
usageBased?: boolean;
|
||||
timeoutMs?: number;
|
||||
}) => {
|
||||
// 1. Send events
|
||||
const allowance = totalAllowance || product.entitlements.metered1.allowance;
|
||||
@@ -35,7 +37,7 @@ const checkEntitledOnProduct = async ({
|
||||
}
|
||||
|
||||
await Promise.all(batchUpdates);
|
||||
await timeout(8000);
|
||||
await timeout(timeoutMs);
|
||||
let used = randomNum;
|
||||
|
||||
// 2. Check entitled
|
||||
@@ -74,7 +76,7 @@ const checkEntitledOnProduct = async ({
|
||||
);
|
||||
}
|
||||
await Promise.all(batchUpdates2);
|
||||
await timeout(8000);
|
||||
await timeout(timeoutMs);
|
||||
used += allowance - randomNum;
|
||||
|
||||
// 3. Check entitled again
|
||||
@@ -124,13 +126,13 @@ describe(`${chalk.yellowBright(
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct entitlements (free)", async function () {
|
||||
await checkEntitledOnProduct({
|
||||
customerId: customerId,
|
||||
product: products.free,
|
||||
finish: true,
|
||||
});
|
||||
});
|
||||
// it("should have correct entitlements (free)", async function () {
|
||||
// await checkEntitledOnProduct({
|
||||
// customerId: customerId,
|
||||
// product: products.free,
|
||||
// finish: true,
|
||||
// });
|
||||
// });
|
||||
|
||||
it("should attach pro", async function () {
|
||||
await AutumnCli.attach({
|
||||
@@ -170,6 +172,7 @@ describe(`${chalk.yellowBright(
|
||||
product: products.oneTimeAddOnMetered1,
|
||||
finish: true,
|
||||
totalAllowance: curAllowance + oneTimeQuantity,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -231,7 +234,7 @@ describe(`${chalk.yellowBright(
|
||||
}
|
||||
|
||||
await Promise.all(batchUpdates);
|
||||
await timeout(14000);
|
||||
await timeout(10000);
|
||||
|
||||
const { allowed: allowed2, balanceObj: balanceObj2 }: any =
|
||||
await AutumnCli.entitled(customerId, features.metered1.id, true);
|
||||
|
||||
@@ -93,7 +93,8 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with customer ID and
|
||||
customer_id: internalCustomerId,
|
||||
entity_id: internalEntityId,
|
||||
product_id: pro.id,
|
||||
invoice_only: true,
|
||||
invoice: true,
|
||||
enable_product_immediately: true,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(internalCustomerId);
|
||||
|
||||
144
server/tests/utils/stripeUtils/completeInvoiceCheckout.ts
Normal file
144
server/tests/utils/stripeUtils/completeInvoiceCheckout.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import "dotenv/config";
|
||||
|
||||
import { Stripe } from "stripe";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import Browserbase from "@browserbasehq/sdk";
|
||||
|
||||
import { Hyperbrowser } from "@hyperbrowser/sdk";
|
||||
import { timeout } from "../genUtils.js";
|
||||
|
||||
const client = new Hyperbrowser({
|
||||
apiKey: process.env.HYPERBROWSER_API_KEY,
|
||||
});
|
||||
|
||||
export const completeInvoiceCheckout = async ({
|
||||
url,
|
||||
isLocal = false,
|
||||
}: {
|
||||
url: string;
|
||||
isLocal?: boolean;
|
||||
}) => {
|
||||
let browser;
|
||||
|
||||
if (process.env.NODE_ENV === "development" && !isLocal) {
|
||||
const session = await client.sessions.create();
|
||||
browser = await puppeteer.connect({
|
||||
browserWSEndpoint: session!.wsEndpoint,
|
||||
defaultViewport: null,
|
||||
});
|
||||
} else {
|
||||
browser = await puppeteer.launch({
|
||||
headless: false,
|
||||
executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
|
||||
await page.goto(url);
|
||||
|
||||
// Wait for the payment element to load
|
||||
|
||||
await page.waitForSelector("#payment-element", { timeout: 10000 });
|
||||
|
||||
// Wait a bit more for the iframe to fully load
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Try clicking on the payment element container to expand the accordion
|
||||
|
||||
await page.click("#payment-element");
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Get the iframe containing the Stripe elements
|
||||
const stripeFrame = await page.$("#payment-element iframe");
|
||||
if (!stripeFrame) {
|
||||
throw new Error("Stripe iframe not found");
|
||||
}
|
||||
|
||||
const frame = await stripeFrame.contentFrame();
|
||||
if (!frame) {
|
||||
throw new Error("Could not access iframe content");
|
||||
}
|
||||
|
||||
// Enter card number - try different possible selectors
|
||||
try {
|
||||
await frame.waitForSelector(
|
||||
'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[placeholder*="1234"], input[aria-label*="Card number"]',
|
||||
{ timeout: 2000 }
|
||||
);
|
||||
const cardNumberInput = await frame.$(
|
||||
'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[placeholder*="1234"], input[aria-label*="Card number"]'
|
||||
);
|
||||
if (cardNumberInput) {
|
||||
await cardNumberInput.click();
|
||||
await cardNumberInput.type("4242424242424242");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Could not find card number input:", error);
|
||||
}
|
||||
|
||||
// Enter expiry date
|
||||
try {
|
||||
await frame.waitForSelector(
|
||||
'input[name="expiry"], input[data-elements-stable-field-name="cardExpiry"], input[placeholder*="MM"], input[aria-label*="expir"]',
|
||||
{ timeout: 2000 }
|
||||
);
|
||||
const expiryInput = await frame.$(
|
||||
'input[name="expiry"], input[data-elements-stable-field-name="cardExpiry"], input[placeholder*="MM"], input[aria-label*="expir"]'
|
||||
);
|
||||
if (expiryInput) {
|
||||
await expiryInput.click();
|
||||
await expiryInput.type("1227");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Could not find expiry input:", error);
|
||||
}
|
||||
|
||||
// Enter CVC
|
||||
try {
|
||||
await frame.waitForSelector(
|
||||
'input[name="cvc"], input[data-elements-stable-field-name="cardCvc"], input[placeholder*="CVC"], input[aria-label*="CVC"]',
|
||||
{ timeout: 2000 }
|
||||
);
|
||||
const cvcInput = await frame.$(
|
||||
'input[name="cvc"], input[data-elements-stable-field-name="cardCvc"], input[placeholder*="CVC"], input[aria-label*="CVC"]'
|
||||
);
|
||||
if (cvcInput) {
|
||||
await cvcInput.click();
|
||||
await cvcInput.type("123");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Could not find CVC input:", error);
|
||||
}
|
||||
|
||||
// Enter postal code
|
||||
try {
|
||||
await frame.waitForSelector(
|
||||
'input[name="postalCode"], input[data-elements-stable-field-name="postalCode"], input[placeholder*="12345"], input[aria-label*="ZIP"]',
|
||||
{ timeout: 2000 }
|
||||
);
|
||||
const postalInput = await frame.$(
|
||||
'input[name="postalCode"], input[data-elements-stable-field-name="postalCode"], input[placeholder*="12345"], input[aria-label*="ZIP"]'
|
||||
);
|
||||
if (postalInput) {
|
||||
await postalInput.click();
|
||||
await postalInput.type("12345");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Could not find postal code input:", error);
|
||||
}
|
||||
|
||||
// Wait a bit for all inputs to be processed
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
const submitButton = await page.$(".SubmitButton-IconContainer");
|
||||
await submitButton?.evaluate((b: any) => (b as HTMLElement).click());
|
||||
await timeout(20000);
|
||||
} finally {
|
||||
// always close browser
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
@@ -44,6 +44,7 @@ export const AttachBodySchema = z
|
||||
checkout_session_params: z.any().optional(),
|
||||
reward: z.string().optional(),
|
||||
invoice: z.boolean().optional(),
|
||||
enable_product_immediately: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum ProrationBehavior {
|
||||
|
||||
export interface AttachConfig {
|
||||
onlyCheckout: boolean;
|
||||
invoiceCheckout: boolean;
|
||||
carryUsage: boolean; // Whether to carry over existing usages
|
||||
branch: AttachBranch;
|
||||
proration: ProrationBehavior;
|
||||
|
||||
@@ -111,7 +111,7 @@ export const CustomerDetails = ({
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faStripe}
|
||||
className="!h-6 text-t2"
|
||||
className="!h-6 !w-6 text-t2"
|
||||
/>
|
||||
<ArrowUpRightFromSquare size={12} className="text-t2" />
|
||||
</Button>
|
||||
|
||||
@@ -31,6 +31,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { AttachInfo } from "./attach-preview/AttachInfo";
|
||||
import { getAttachBody } from "./attachProductUtils";
|
||||
import { InvoiceCustomerButton } from "./InvoiceCustomerButton";
|
||||
|
||||
export const AttachModal = ({
|
||||
open,
|
||||
@@ -118,8 +119,15 @@ export const AttachModal = ({
|
||||
return "Charge Customer";
|
||||
};
|
||||
|
||||
const handleAttachClicked = async (useInvoice: boolean) => {
|
||||
const setLoading = useInvoice ? setInvoiceLoading : setCheckoutLoading;
|
||||
const handleAttachClicked = async ({
|
||||
useInvoice,
|
||||
enableProductImmediately,
|
||||
setLoading,
|
||||
}: {
|
||||
useInvoice: boolean;
|
||||
enableProductImmediately?: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}) => {
|
||||
const cusId = getCusId();
|
||||
|
||||
for (const option of options) {
|
||||
@@ -145,6 +153,7 @@ export const AttachModal = ({
|
||||
optionsInput: options,
|
||||
attachState,
|
||||
useInvoice,
|
||||
enableProductImmediately,
|
||||
successUrl: `${import.meta.env.VITE_FRONTEND_URL}${redirectUrl}`,
|
||||
version: version || product.version,
|
||||
});
|
||||
@@ -155,9 +164,8 @@ export const AttachModal = ({
|
||||
window.open(data.checkout_url, "_blank");
|
||||
} else if (data.invoice) {
|
||||
window.open(getStripeInvoiceLink(data.invoice), "_blank");
|
||||
} else {
|
||||
navigateTo(`/customers/${cusId}`, navigation, env);
|
||||
}
|
||||
navigateTo(`/customers/${cusId}`, navigation, env);
|
||||
|
||||
toast.success(data.message || "Successfully attached product");
|
||||
setOpen(false);
|
||||
@@ -186,7 +194,7 @@ export const AttachModal = ({
|
||||
const mainWidth = "w-lg";
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="gap-0 p-0 rounded-xs">
|
||||
<DialogContent className="translate-y-[0%] top-[20%] max-h-[70vh] duration-0 p-0 overflow-y-auto">
|
||||
<div className="flex transition-all duration-300 ease-in-out">
|
||||
<div
|
||||
className={`p-6 pb-2 flex flex-col gap-4 ${mainWidth} rounded-sm`}
|
||||
@@ -234,19 +242,23 @@ export const AttachModal = ({
|
||||
)}
|
||||
>
|
||||
{invoiceAllowed() && (
|
||||
<Button
|
||||
variant="add"
|
||||
className="!h-full text-t2"
|
||||
endIcon={<ArrowUpRightFromSquare size={12} />}
|
||||
disableStartIcon={true}
|
||||
tabIndex={-1}
|
||||
tooltipContent="This will enable the product for the customer immediately, and redirect you to Stripe to finalize the invoice"
|
||||
isLoading={invoiceLoading}
|
||||
disabled={invoiceLoading || checkoutLoading}
|
||||
onClick={() => handleAttachClicked(true)}
|
||||
>
|
||||
Invoice Customer
|
||||
</Button>
|
||||
<InvoiceCustomerButton
|
||||
preview={preview}
|
||||
handleAttachClicked={handleAttachClicked}
|
||||
/>
|
||||
// <Button
|
||||
// variant="add"
|
||||
// className="!h-full text-t2"
|
||||
// endIcon={<ArrowUpRightFromSquare size={12} />}
|
||||
// disableStartIcon={true}
|
||||
// tabIndex={-1}
|
||||
// tooltipContent="This will enable the product for the customer immediately, and redirect you to Stripe to finalize the invoice"
|
||||
// isLoading={invoiceLoading}
|
||||
// disabled={invoiceLoading || checkoutLoading}
|
||||
// onClick={() => handleAttachClicked(true)}
|
||||
// >
|
||||
// Invoice Customer
|
||||
// </Button>
|
||||
)}
|
||||
<Button
|
||||
variant="add"
|
||||
@@ -255,7 +267,12 @@ export const AttachModal = ({
|
||||
endIcon={<ArrowUpRightFromSquare size={12} />}
|
||||
isLoading={checkoutLoading}
|
||||
disabled={invoiceLoading || checkoutLoading}
|
||||
onClick={() => handleAttachClicked(false)}
|
||||
onClick={() =>
|
||||
handleAttachClicked({
|
||||
useInvoice: false,
|
||||
setLoading: setCheckoutLoading,
|
||||
})
|
||||
}
|
||||
>
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import FieldLabel from "@/components/general/modal-components/FieldLabel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { AttachBranch, AttachFunction } from "@autumn/shared";
|
||||
import { ArrowUpRightFromSquare } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const InvoiceCustomerButton = ({
|
||||
handleAttachClicked,
|
||||
preview,
|
||||
}: {
|
||||
handleAttachClicked: any;
|
||||
preview?: any;
|
||||
}) => {
|
||||
const [immediateLoading, setImmediateLoading] = useState(false);
|
||||
const [afterPaymentLoading, setAfterPaymentLoading] = useState(false);
|
||||
const buttonsDisabled = immediateLoading || afterPaymentLoading;
|
||||
|
||||
const allowedBranches = [
|
||||
AttachBranch.New,
|
||||
AttachBranch.MainIsTrial,
|
||||
AttachBranch.MainIsFree,
|
||||
AttachBranch.OneOff,
|
||||
AttachBranch.AddOn,
|
||||
];
|
||||
|
||||
// const immediateDisabled = !allowedBranches.includes(preview?.branch);
|
||||
|
||||
console.log("Preview:", preview);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="add"
|
||||
className="!h-full text-t2"
|
||||
endIcon={<ArrowUpRightFromSquare size={12} />}
|
||||
disableStartIcon={true}
|
||||
tabIndex={-1}
|
||||
tooltipContent="This will enable the product for the customer immediately, and redirect you to Stripe to finalize the invoice"
|
||||
>
|
||||
Invoice Customer
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit flex p-0">
|
||||
<div className="w-[300px]">
|
||||
<div className="border-r p-4 text-sm flex flex-col gap-2">
|
||||
<p>Enable Product Immediately</p>
|
||||
<p className="text-t2">
|
||||
This will enable the product for the customer immediately, and
|
||||
redirect you to Stripe to finalize the invoice
|
||||
</p>
|
||||
<Button
|
||||
isLoading={immediateLoading}
|
||||
className="w-fit mt-2"
|
||||
variant="outline"
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() =>
|
||||
handleAttachClicked({
|
||||
useInvoice: true,
|
||||
enableProductImmediately: true,
|
||||
setLoading: setImmediateLoading,
|
||||
})
|
||||
}
|
||||
>
|
||||
Invoice and enable immediately
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{preview?.func == AttachFunction.CreateCheckout && (
|
||||
<div className="w-[300px]">
|
||||
<div className="p-4 text-sm flex flex-col gap-2">
|
||||
<p>Enable Product After Payment</p>
|
||||
<p className="text-t2">
|
||||
This will generate an invoice link for the customer, and enable
|
||||
the product after they pay the invoice
|
||||
</p>
|
||||
<Button
|
||||
isLoading={afterPaymentLoading}
|
||||
className="w-fit mt-2"
|
||||
variant="outline"
|
||||
disabled={buttonsDisabled}
|
||||
onClick={() =>
|
||||
handleAttachClicked({
|
||||
useInvoice: true,
|
||||
enableProductImmediately: false,
|
||||
setLoading: setAfterPaymentLoading,
|
||||
})
|
||||
}
|
||||
>
|
||||
Invoice and enable after payment
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ export const getAttachBody = ({
|
||||
entityId,
|
||||
optionsInput,
|
||||
useInvoice,
|
||||
enableProductImmediately = true,
|
||||
successUrl,
|
||||
version,
|
||||
}: {
|
||||
@@ -22,6 +23,7 @@ export const getAttachBody = ({
|
||||
entityId: string;
|
||||
optionsInput?: FeatureOptions[];
|
||||
useInvoice?: boolean;
|
||||
enableProductImmediately?: boolean;
|
||||
successUrl?: string;
|
||||
version?: number;
|
||||
}) => {
|
||||
@@ -47,7 +49,14 @@ export const getAttachBody = ({
|
||||
...customData,
|
||||
free_trial: isCustom ? product.free_trial || undefined : undefined,
|
||||
|
||||
invoice_only: useInvoice,
|
||||
invoice: useInvoice,
|
||||
enable_product_immediately: useInvoice
|
||||
? enableProductImmediately
|
||||
: undefined,
|
||||
|
||||
force_checkout:
|
||||
useInvoice && enableProductImmediately === false ? true : undefined,
|
||||
|
||||
success_url: successUrl,
|
||||
version: version ? Number(version) : undefined,
|
||||
};
|
||||
|
||||
@@ -11,31 +11,31 @@ export const useMemberships = () => {
|
||||
url: "/organization/members",
|
||||
});
|
||||
|
||||
const { data: session } = useSession();
|
||||
const { data: orgs } = useListOrganizations();
|
||||
// const { data: session } = useSession();
|
||||
// const { data: orgs } = useListOrganizations();
|
||||
|
||||
const handleRemovedFromOrg = async () => {
|
||||
const inOrg = orgs?.find(
|
||||
(org: any) => org.id === session?.session?.activeOrganizationId,
|
||||
);
|
||||
// const handleRemovedFromOrg = async () => {
|
||||
// const inOrg = orgs?.find(
|
||||
// (org: any) => org.id === session?.session?.activeOrganizationId,
|
||||
// );
|
||||
|
||||
if (!inOrg) {
|
||||
if (orgs && orgs.length > 0) {
|
||||
await authClient.organization.setActive({
|
||||
organizationId: orgs[0].id,
|
||||
});
|
||||
} else {
|
||||
const { data, error } = await authClient.revokeSessions();
|
||||
console.log("Revoked sessions", data, error);
|
||||
}
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
// if (!inOrg) {
|
||||
// if (orgs && orgs.length > 0) {
|
||||
// await authClient.organization.setActive({
|
||||
// organizationId: orgs[0].id,
|
||||
// });
|
||||
// } else {
|
||||
// const { data, error } = await authClient.revokeSessions();
|
||||
// console.log("Revoked sessions", data, error);
|
||||
// }
|
||||
// window.location.reload();
|
||||
// }
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
if (!orgs) return;
|
||||
handleRemovedFromOrg();
|
||||
}, [orgs]);
|
||||
// useEffect(() => {
|
||||
// if (!orgs) return;
|
||||
// handleRemovedFromOrg();
|
||||
// }, [orgs]);
|
||||
|
||||
return {
|
||||
memberships: data?.memberships || [],
|
||||
|
||||
Reference in New Issue
Block a user