added product group functionality

This commit is contained in:
John Yeo
2025-01-27 19:40:39 +00:00
parent 8028ca54e7
commit e367f2bf91
6 changed files with 89 additions and 31 deletions

View File

@@ -58,6 +58,7 @@ export const handleCheckoutSessionCompleted = async ({
await CusProductService.expireCurrentProduct({
sb,
internalCustomerId: customer.internal_id,
productGroup: product.group,
});
console.log("Creating full customer product");

View File

@@ -43,6 +43,7 @@ export const handleSubscriptionDeleted = async ({
const futureProduct = await CusProductService.activateFutureProduct({
sb,
internalCustomerId: cusProduct.internal_customer_id,
productGroup: cusProduct.product.group,
});
if (futureProduct) {

View File

@@ -105,17 +105,18 @@ const handleExistingProduct = async ({
product: FullProduct;
}) => {
const { sb } = req;
const existingCusProduct = await CusProductService.getCurrentProduct({
const currentProduct = await CusProductService.getCurrentProductByGroup({
sb,
internalCustomerId: customer.internal_id,
productGroup: product.group,
});
// 2. Don't allow customer to get multiple of the same product
if (existingCusProduct?.internal_product_id === product.internal_id) {
if (currentProduct?.product.internal_id === product.internal_id) {
// If there's a future product, delete, else
const deletedCusProduct = await CusProductService.deleteFutureProduct({
sb,
internalCustomerId: customer.internal_id,
productGroup: product.group,
});
if (deletedCusProduct) {
@@ -126,9 +127,9 @@ const handleExistingProduct = async ({
);
}
// Continue current product subscription
if (existingCusProduct.processor.subscription_id) {
if (currentProduct.processor.subscription_id) {
await stripeCli.subscriptions.update(
existingCusProduct.processor.subscription_id,
currentProduct.processor.subscription_id,
{
cancel_at_period_end: false,
}
@@ -146,7 +147,7 @@ const handleExistingProduct = async ({
return true;
} else {
throw new RecaseError({
message: `Customer ${customer.id} already has product ${existingCusProduct.product_id}`,
message: `Customer ${customer.id} already has product ${currentProduct.product_id}`,
code: ErrCode.CustomerAlreadyHasProduct,
statusCode: 400,
});
@@ -154,7 +155,7 @@ const handleExistingProduct = async ({
}
// 3. If no existing product, check if new product is add-on
if (!existingCusProduct && product.is_add_on) {
if (!currentProduct && product.is_add_on) {
throw new RecaseError({
message: `Customer has no base product`,
code: ErrCode.CustomerHasNoBaseProduct,
@@ -162,7 +163,7 @@ const handleExistingProduct = async ({
});
}
return existingCusProduct;
return currentProduct;
};
const checkStripeConnections = async ({

View File

@@ -198,21 +198,25 @@ export const expireOrDeleteCusProduct = async ({
sb,
customer,
startsAt,
productGroup,
}: {
sb: SupabaseClient;
customer: Customer;
startsAt?: number;
productGroup: string;
}) => {
// 1. If startsAt
if (startsAt && startsAt > Date.now()) {
await CusProductService.deleteFutureProduct({
sb,
internalCustomerId: customer.internal_id,
productGroup,
});
} else {
await CusProductService.expireCurrentProduct({
sb,
internalCustomerId: customer.internal_id,
productGroup,
});
}
};
@@ -247,6 +251,7 @@ export const createFullCusProduct = async ({
sb,
customer,
startsAt,
productGroup: product.group,
});
}

View File

@@ -25,6 +25,7 @@ import { createFullCusProduct } from "../add-product/createFullCusProduct.js";
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { InvoiceService } from "../invoices/InvoiceService.js";
import { handleAddProduct } from "../add-product/handleAddProduct.js";
import { CusProductService } from "../products/CusProductService.js";
const scheduleStripeSubscription = async ({
customer,
@@ -123,7 +124,17 @@ const handleDowngrade = async ({
});
for (const schedule of schedules.data) {
if (schedule.status !== "canceled") {
const existingCusProduct = await CusProductService.getByScheduleId({
sb: req.sb,
scheduleId: schedule.id,
});
// Delete only if not in the same group
if (
(!existingCusProduct ||
existingCusProduct.product.group === product.group) &&
schedule.status !== "canceled"
) {
await stripeCli.subscriptionSchedules.cancel(schedule.id);
}
}

View File

@@ -65,38 +65,33 @@ export class CusProductService {
return data;
}
static async getCurrentProduct({
static async getCurrentProductByGroup({
sb,
internalCustomerId,
productGroup,
}: {
sb: SupabaseClient;
internalCustomerId: string;
productGroup: string;
}) {
const { data, error } = await sb
.from("customer_products")
.select("*, product:products!inner(*)")
.eq("internal_customer_id", internalCustomerId)
.eq("product.group", productGroup)
.eq("product.is_add_on", false)
.neq("status", CusProductStatus.Expired)
.neq("status", CusProductStatus.Scheduled);
.neq("status", CusProductStatus.Scheduled)
.single();
if (error) {
if (error.code === "PGRST116") {
return null;
}
throw error;
}
if (data.length > 1) {
throw new RecaseError({
message: "Multiple products found for customer",
code: ErrCode.MultipleProductsFound,
statusCode: 500,
});
}
if (data.length === 0) {
return null;
}
return data[0];
return data;
}
static async getByCusAndProductId({
@@ -185,6 +180,29 @@ export class CusProductService {
return data;
}
static async getByScheduleId({
sb,
scheduleId,
}: {
sb: SupabaseClient;
scheduleId: string;
}) {
const { data, error } = await sb
.from("customer_products")
.select("*, product:products!inner(*)")
.eq("processor->>subscription_schedule_id", scheduleId)
.single();
if (error) {
if (error.code === "PGRST116") {
return null;
}
throw error;
}
return data;
}
static async updateStatusByStripeSubId({
sb,
stripeSubId,
@@ -242,13 +260,17 @@ export class CusProductService {
static async expireCurrentProduct({
sb,
internalCustomerId,
productGroup,
}: {
sb: SupabaseClient;
internalCustomerId: string;
productGroup: string;
}) {
const currentProduct = await this.getCurrentProduct({
// TO WORK ON EXPIRING
const currentProduct = await this.getCurrentProductByGroup({
sb,
internalCustomerId,
productGroup,
});
if (!currentProduct) {
@@ -271,9 +293,11 @@ export class CusProductService {
static async activateFutureProduct({
sb,
internalCustomerId,
productGroup,
}: {
sb: SupabaseClient;
internalCustomerId: string;
productGroup: string;
}) {
const { data, error } = await sb
.from("customer_products")
@@ -281,6 +305,7 @@ export class CusProductService {
status: CusProductStatus.Active,
})
.eq("internal_customer_id", internalCustomerId)
.eq("product.group", productGroup)
.eq("status", CusProductStatus.Scheduled)
.select()
.single();
@@ -300,25 +325,39 @@ export class CusProductService {
static async deleteFutureProduct({
sb,
internalCustomerId,
productGroup,
}: {
sb: SupabaseClient;
internalCustomerId: string;
productGroup: string;
}) {
// 1. Get all products in same group
const { data, error } = await sb
.from("customer_products")
.delete()
.select("*, product:products!inner(*)")
.eq("internal_customer_id", internalCustomerId)
.eq("status", CusProductStatus.Scheduled)
.eq("product.group", productGroup)
.eq("status", CusProductStatus.Scheduled);
if (!data || data.length === 0) {
return null;
}
// Delete product
const { error: deleteError } = await sb
.from("customer_products")
.delete()
.eq("id", data[0].id)
.select()
.single();
if (error) {
if (error.code === "PGRST116") {
if (deleteError) {
if (deleteError.code === "PGRST116") {
return null;
}
throw error;
throw deleteError;
}
return data;
return data[0];
}
}