fix: ent rollover null / undefined difference
This commit is contained in:
@@ -13,6 +13,7 @@ import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import Stripe from "stripe";
|
||||
import { billingIntervalToStripe } from "../stripePriceUtils.js";
|
||||
import {
|
||||
formatPrice,
|
||||
getBillingType,
|
||||
getPriceEntitlement,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
|
||||
@@ -22,6 +22,7 @@ import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCo
|
||||
import { PriceService } from "../products/prices/PriceService.js";
|
||||
import { OrgService } from "../orgs/OrgService.js";
|
||||
import { formatPrice } from "../products/prices/priceUtils.js";
|
||||
import { ProductService } from "../products/ProductService.js";
|
||||
|
||||
// Helper function to check if tier structures match
|
||||
const tiersMatch = (oldTiers: any[], newTiers: any[]): boolean => {
|
||||
@@ -83,6 +84,8 @@ const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => {
|
||||
// First, filter by basic characteristics
|
||||
|
||||
const candidates = newPrices.filter((newPrice) => {
|
||||
if (newPrice.id === oldPrice.id) return true;
|
||||
|
||||
const oldConfig = oldPrice.config as UsagePriceConfig;
|
||||
const newConfig = newPrice.config as UsagePriceConfig;
|
||||
|
||||
@@ -96,12 +99,6 @@ const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => {
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Candidates: ",
|
||||
candidates.map((p) => formatPrice({ price: p }))
|
||||
);
|
||||
console.log("--------------------------------");
|
||||
|
||||
if (candidates.length === 0) return null;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
@@ -128,17 +125,27 @@ export async function runRewardMigrationTask({
|
||||
try {
|
||||
const {
|
||||
oldPrices,
|
||||
newPrices,
|
||||
productId,
|
||||
// newPrices,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
oldPrices: Price[];
|
||||
newPrices: Price[];
|
||||
product: FullProduct;
|
||||
// newPrices: Price[];
|
||||
productId: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
} = payload;
|
||||
|
||||
const fullProduct = await ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: productId,
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
|
||||
const newPrices = fullProduct.prices;
|
||||
|
||||
// Get organization for Stripe operations
|
||||
const org = await OrgService.get({
|
||||
db,
|
||||
@@ -167,10 +174,8 @@ export async function runRewardMigrationTask({
|
||||
)
|
||||
);
|
||||
|
||||
// console.log(
|
||||
// "New price IDs: ",
|
||||
// newPrices.map((p) => formatPrice({ price: p }))
|
||||
// );
|
||||
let shouldUpdateReward = false;
|
||||
|
||||
for (const reward of filteredRewards) {
|
||||
const newPriceIds: string[] = [];
|
||||
const unmatchedPrices: string[] = [];
|
||||
@@ -178,8 +183,10 @@ export async function runRewardMigrationTask({
|
||||
if (reward.discount_config?.price_ids) {
|
||||
for (const priceId of reward.discount_config.price_ids) {
|
||||
const oldPrice = oldPrices.find((p) => p.id === priceId);
|
||||
|
||||
// From other product
|
||||
if (!oldPrice) {
|
||||
logger.warn(`Old price ${priceId} not found in oldPrices array`);
|
||||
newPriceIds.push(priceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -187,23 +194,28 @@ export async function runRewardMigrationTask({
|
||||
|
||||
if (matchingNewPrice) {
|
||||
newPriceIds.push(matchingNewPrice.id);
|
||||
const shouldUpdate =
|
||||
matchingNewPrice.config.stripe_price_id !==
|
||||
oldPrice.config.stripe_price_id ||
|
||||
matchingNewPrice.config.stripe_product_id !==
|
||||
oldPrice.config.stripe_product_id;
|
||||
|
||||
if (shouldUpdate) {
|
||||
shouldUpdateReward = true;
|
||||
}
|
||||
} else {
|
||||
unmatchedPrices.push(oldPrice.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("New price IDs: ", newPriceIds);
|
||||
throw new Error("test");
|
||||
|
||||
// Update the reward with new price IDs
|
||||
if (newPriceIds.length > 0) {
|
||||
if (shouldUpdateReward) {
|
||||
try {
|
||||
// Check if price IDs have actually changed
|
||||
const originalPriceIds = reward.discount_config?.price_ids || [];
|
||||
const priceIdsChanged =
|
||||
originalPriceIds.length !== newPriceIds.length ||
|
||||
!originalPriceIds.every((id) => newPriceIds.includes(id));
|
||||
// Update Stripe coupon and reward if price IDs have changed
|
||||
console.log(
|
||||
`Updating ${reward.id}, updating reward and Stripe coupon...`
|
||||
);
|
||||
|
||||
// Update the reward in the database
|
||||
const updatedReward = await RewardService.update({
|
||||
@@ -219,13 +231,6 @@ export async function runRewardMigrationTask({
|
||||
},
|
||||
});
|
||||
|
||||
// Update Stripe coupon if price IDs have changed
|
||||
if (priceIdsChanged && org) {
|
||||
try {
|
||||
logger.info(
|
||||
`Price IDs changed for reward ${reward.id}, updating Stripe coupon...`
|
||||
);
|
||||
|
||||
// Get the price objects for the new price IDs
|
||||
const prices = await PriceService.getInIds({
|
||||
db,
|
||||
@@ -241,52 +246,23 @@ export async function runRewardMigrationTask({
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
console.log(
|
||||
`Successfully updated Stripe coupon for reward ${reward.id} with new product restrictions`
|
||||
);
|
||||
} catch (stripeError) {
|
||||
logger.error(
|
||||
`Failed to update Stripe coupon for reward ${reward.id}:`,
|
||||
stripeError
|
||||
);
|
||||
// Don't throw here - we want to continue with other rewards
|
||||
}
|
||||
}
|
||||
logger.info(
|
||||
`Updated reward "${reward.name}" (${reward.id}) with ${newPriceIds.length} prices`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to update reward ${reward.id}:`, error);
|
||||
console.error(`Failed to update reward ${reward.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (unmatchedPrices.length > 0) {
|
||||
logger.warn(
|
||||
console.warn(
|
||||
`Unmatched prices for reward ${reward.id}:`,
|
||||
unmatchedPrices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Migration summary
|
||||
const totalRewards = filteredRewards.length;
|
||||
const updatedRewards = filteredRewards.filter((r) =>
|
||||
r.discount_config?.price_ids?.some(
|
||||
(priceId) =>
|
||||
oldPrices.find((p) => p.id === priceId) &&
|
||||
findBestMatch(oldPrices.find((p) => p.id === priceId)!, newPrices)
|
||||
)
|
||||
).length;
|
||||
|
||||
logger.info("================================");
|
||||
logger.info("REWARD MIGRATION SUMMARY FOR ORG: ", orgId);
|
||||
logger.info("================================");
|
||||
logger.info(`Total rewards processed: ${totalRewards}`);
|
||||
logger.info(`Rewards with successful matches: ${updatedRewards}`);
|
||||
logger.info(`Rewards with no matches: ${totalRewards - updatedRewards}`);
|
||||
logger.info("================================");
|
||||
} catch (error) {
|
||||
logger.error("Error running reward migration task", { error });
|
||||
console.error("Error running reward migration task", { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
Price,
|
||||
FullProduct,
|
||||
FullEntitlement,
|
||||
Rollover,
|
||||
RolloverConfig,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { addDays } from "date-fns";
|
||||
@@ -76,6 +78,24 @@ export const addTrialToNextResetAt = (
|
||||
return addDays(new Date(nextResetAt), freeTrial.length).getTime();
|
||||
};
|
||||
|
||||
export const rolloversAreSame = ({
|
||||
rollover1,
|
||||
rollover2,
|
||||
}: {
|
||||
rollover1?: RolloverConfig | null;
|
||||
rollover2?: RolloverConfig | null;
|
||||
}) => {
|
||||
if (!rollover1 && !rollover2) return true;
|
||||
if (!rollover1 && rollover2) return false;
|
||||
if (rollover1 && !rollover2) return false;
|
||||
|
||||
return (
|
||||
rollover1!.max == rollover2!.max &&
|
||||
rollover1!.duration == rollover2!.duration &&
|
||||
rollover1!.length == rollover2!.length
|
||||
);
|
||||
};
|
||||
|
||||
export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
|
||||
// 1. Check if they have same internal_feature_id
|
||||
if (ent1.internal_feature_id !== ent2.internal_feature_id) {
|
||||
@@ -121,23 +141,25 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
|
||||
message: `Usage limit different: ${ent1.usage_limit} !== ${ent2.usage_limit}`,
|
||||
},
|
||||
rollover: {
|
||||
condition:
|
||||
JSON.stringify(ent1.rollover) !== JSON.stringify(ent2.rollover),
|
||||
condition: !rolloversAreSame({
|
||||
rollover1: ent1.rollover,
|
||||
rollover2: ent2.rollover,
|
||||
}),
|
||||
message: `Rollover different: ${ent1.rollover} !== ${ent2.rollover}`,
|
||||
},
|
||||
};
|
||||
|
||||
let entsAreDiff = Object.values(diffs).some((d) => d.condition);
|
||||
|
||||
// if (entsAreDiff) {
|
||||
// console.log("Entitlements different");
|
||||
// console.log(
|
||||
// "Differences:",
|
||||
// Object.values(diffs)
|
||||
// .filter((d) => d.condition)
|
||||
// .map((d) => d.message),
|
||||
// );
|
||||
// }
|
||||
if (entsAreDiff) {
|
||||
console.log("Entitlements different");
|
||||
console.log(
|
||||
"Differences:",
|
||||
Object.values(diffs)
|
||||
.filter((d) => d.condition)
|
||||
.map((d) => d.message)
|
||||
);
|
||||
}
|
||||
return !entsAreDiff;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "../handleCreateProduct.js";
|
||||
import { handleVersionProductV2 } from "../handleVersionProduct.js";
|
||||
import { handleUpdateProductDetails } from "./updateProductDetails.js";
|
||||
import { formatPrice } from "../../prices/priceUtils.js";
|
||||
|
||||
export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
routeHandler({
|
||||
@@ -152,7 +153,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
});
|
||||
}
|
||||
|
||||
const { prices, entitlements } = await handleNewProductItems({
|
||||
await handleNewProductItems({
|
||||
db,
|
||||
curPrices: fullProduct.prices,
|
||||
curEnts: fullProduct.entitlements,
|
||||
@@ -163,9 +164,17 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
isCustom: false,
|
||||
});
|
||||
|
||||
// New full product
|
||||
const newFullProduct = await ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: fullProduct.id,
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
|
||||
if (free_trial !== undefined) {
|
||||
await validateOneOffTrial({
|
||||
prices,
|
||||
prices: newFullProduct.prices,
|
||||
freeTrial: free_trial,
|
||||
});
|
||||
|
||||
@@ -179,13 +188,10 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
});
|
||||
}
|
||||
|
||||
// New full product
|
||||
await initProductInStripe({
|
||||
db,
|
||||
product: {
|
||||
...fullProduct,
|
||||
prices,
|
||||
entitlements,
|
||||
} as FullProduct,
|
||||
product: newFullProduct,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
@@ -195,11 +201,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
await addTaskToQueue({
|
||||
jobName: JobName.DetectBaseVariant,
|
||||
payload: {
|
||||
curProduct: {
|
||||
...fullProduct,
|
||||
prices: prices.length > 0 ? prices : fullProduct.prices,
|
||||
entitlements,
|
||||
},
|
||||
curProduct: newFullProduct,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -207,12 +209,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
|
||||
jobName: JobName.RewardMigration,
|
||||
payload: {
|
||||
oldPrices: fullProduct.prices,
|
||||
newPrices: prices,
|
||||
product: {
|
||||
...fullProduct,
|
||||
prices,
|
||||
entitlements: getEntsWithFeature({ ents: entitlements, features }),
|
||||
},
|
||||
productId: fullProduct.id,
|
||||
orgId: org.id,
|
||||
env,
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import { validateProductItems } from "../validateProductItems.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { isFeatureItem } from "./getItemType.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { formatPrice } from "../../prices/priceUtils.js";
|
||||
|
||||
const updateDbPricesAndEnts = async ({
|
||||
db,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
AllowanceType,
|
||||
BillingInterval,
|
||||
@@ -19,13 +20,12 @@ import {
|
||||
OnIncrease,
|
||||
OnDecrease,
|
||||
FeatureUsageType,
|
||||
features,
|
||||
} from "@autumn/shared";
|
||||
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { pricesAreSame } from "@/internal/products/prices/priceInitUtils.js";
|
||||
import { entsAreSame } from "../../entitlements/entitlementUtils.js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
import {
|
||||
isFeatureItem,
|
||||
isFeaturePriceItem,
|
||||
|
||||
@@ -6,8 +6,9 @@ import { QueueManager } from "./QueueManager.js";
|
||||
export interface Payloads {
|
||||
[JobName.RewardMigration]: {
|
||||
oldPrices: Price[];
|
||||
newPrices: Price[];
|
||||
product: FullProduct;
|
||||
productId: string;
|
||||
// newPrices: Price[];
|
||||
// product: FullProduct;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user