fix: customise interval UI, carry existing usage diff intervals

This commit is contained in:
John Yeo
2025-09-10 21:45:37 -07:00
parent de14c42770
commit 079b4d8fb5
9 changed files with 48 additions and 46 deletions

View File

@@ -7,6 +7,7 @@ source "$(dirname "$0")/config.sh"
if [[ "$1" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
# $MOCHA_CMD 'tests/advanced/multiFeature/*.ts'
$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \

View File

@@ -23,7 +23,7 @@ import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
import { getHasProrations } from "./getHasProrations.js";
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
import { z } from "zod";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
const getAttachVars = async ({
req,

View File

@@ -151,8 +151,8 @@ export const previewToCheckoutRes = async ({
return CheckoutResponseSchema.parse({
customer_id: attachParams.customer.id,
lines,
product: newProduct,
current_product: curProduct,
product: notNullish(attachParams.products) ? undefined : newProduct,
current_product: notNullish(attachParams.products) ? undefined : curProduct,
total: new Decimal(total).toDecimalPlaces(2).toNumber(),
currency: org.default_currency || "usd",
next_cycle_at: notNullish(preview.due_next_cycle)

View File

@@ -67,6 +67,9 @@ export const getExistingUsages = ({
let usages: Record<
string,
{
feature_id: string;
interval: EntInterval;
interval_count: number;
usage: number;
entityUsages: Record<string, number> | null;
fromEntities: boolean;
@@ -84,6 +87,9 @@ export const getExistingUsages = ({
if (!usages[key]) {
usages[key] = {
feature_id: feature?.id || "",
interval: EntInterval.Lifetime,
interval_count: 1,
usage: 0,
entityUsages: null,
fromEntities: true,
@@ -97,21 +103,20 @@ export const getExistingUsages = ({
let ent = cusEnt.entitlement;
let key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
let feature = ent.feature;
if (feature.type == FeatureType.Boolean) {
continue;
}
if (feature.type == FeatureType.Boolean) continue;
let { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
cusEnts: curCusProduct.customer_entitlements,
internalFeatureId: ent.internal_feature_id!,
});
if (unlimited) {
continue;
}
if (unlimited) continue;
if (!usages[key]) {
usages[key] = {
feature_id: feature.id,
interval: ent.interval || EntInterval.Lifetime,
interval_count: ent.interval_count || 1,
usage: 0,
entityUsages: null,
fromEntities: false,
@@ -191,7 +196,7 @@ export const addExistingUsagesToCusEnts = ({
// Sort cusEnts
sortCusEntsForDeduction(fullCusEnts);
// printLogs = true;
printLogs = false;
if (printLogs) {
console.log("DEDUCTING EXISTING USAGE FROM CUS ENTS");
console.log("Existing usages:", existingUsages);
@@ -204,30 +209,32 @@ export const addExistingUsagesToCusEnts = ({
);
}
// Perform deductions...
for (const key in existingUsages) {
let usage = existingUsages[key].usage;
let entityUsages = existingUsages[key].entityUsages;
const {
feature_id = "",
interval = "",
interval_count = 1,
} = existingUsages[key] || {};
for (const cusEnt of fullCusEnts) {
let ent = cusEnt.entitlement;
let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
// let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
let fromEntities = existingUsages[key].fromEntities;
if (cusEntKey !== key) {
continue;
}
// if (cusEntKey !== key) continue;
const isSameFeature = cusEnt.feature_id == feature_id;
if (!isSameFeature) continue;
let shouldCarry =
ent.carry_from_previous || carryExistingUsages || fromEntities;
if (!shouldCarry) {
continue;
}
if (!shouldCarry) continue;
if (notNullish(entityUsages)) {
// TODO: Check if this works...
for (const entityId in entityUsages) {
let { toDeduct, newEntities } = performDeductionOnCusEnt({
cusEnt,

View File

@@ -99,7 +99,7 @@ export const getLifetimeAndUsageCusEnts = async ({
const testCase = "multiFeature2";
describe(`${chalk.yellowBright(
"multiFeature2: Testing lifetime + pay per use -> pay per use",
"multiFeature2: Testing lifetime + pay per use -> pay per use"
)}`, () => {
let autumn: AutumnInt = new AutumnInt();
let autumn2: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
@@ -179,7 +179,7 @@ describe(`${chalk.yellowBright(
});
expect(lifetimeCusEnt?.balance).to.equal(
(pro.items.lifetime.included_usage as number) - value,
(pro.items.lifetime.included_usage as number) - value
);
expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
});
@@ -193,13 +193,14 @@ describe(`${chalk.yellowBright(
feature_id: features.metered1.id,
});
await timeout(4000);
await timeout(3000);
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
});
// return;
let { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } =
await getLifetimeAndUsageCusEnts({
customerId,
@@ -210,9 +211,7 @@ describe(`${chalk.yellowBright(
});
expect(lifetimeCusEnt).to.not.exist;
expect(newUsageCusEnt?.balance).to.equal(
premium.items.payPerUse.included_usage,
);
expect(newUsageCusEnt?.balance).to.equal(-50);
// Check invoice too
let res = await autumn2.customers.get(customerId);
@@ -221,7 +220,7 @@ describe(`${chalk.yellowBright(
let invoice0Amount = value * (pro.items.payPerUse.price ?? 0);
expect(invoices![0].total).to.equal(
invoice0Amount,
"Invoice 0 should be 0",
"Invoice 0 should be 0"
);
});
});

View File

@@ -11,6 +11,8 @@ import {
} from "@/components/ui/select";
import { toast } from "sonner";
import { Reward, RewardType } from "@autumn/shared";
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
import { useOrg } from "@/hooks/common/useOrg";
export const AddRewardButton = ({
setAttachRewards,
@@ -44,7 +46,8 @@ export const MultiAttachRewards = ({
setAttachRewards: (rewards: any) => void;
sub: any;
}) => {
const { rewards, org } = useCustomerContext();
const { org } = useOrg();
const { rewards } = useRewardsQuery();
const subDiscounts = sub?.discounts || [];
const noRewards = attachRewards.length === 0 && subDiscounts.length === 0;

View File

@@ -7,10 +7,8 @@ import ProductViewBreadcrumbs from "./components/ProductViewBreadcrumbs";
import { useState } from "react";
import { ProductContext } from "./ProductContext";
import { useParams, useSearchParams } from "react-router";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useParams } from "react-router";
import { ManageProduct } from "./ManageProduct";
import { AppEnv } from "@autumn/shared";
import { useProductChangedAlert } from "./hooks/useProductChangedAlert";
import { useProductData } from "./hooks/useProductData";
import { UpdateProductButton } from "./components/UpdateProductButton";
@@ -130,7 +128,7 @@ function ProductView() {
</div>
</div>
</div>
<div className="flex max-w-md w-1/3 shrink-1 lg:block lg:min-w-xs sticky top-0">
<div className="hidden lg:flex max-w-md w-1/3 shrink-1 lg:min-w-xs sticky top-0">
<ProductSidebar />
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useRef } from "react";
import { useProductItemContext } from "../../ProductItemContext";
import FieldLabel from "@/components/general/modal-components/FieldLabel";
import { Input } from "@/components/ui/input";
@@ -14,6 +14,7 @@ export const CustomiseIntervalPopover = () => {
const [open, setOpen] = useState(false);
const { item, setItem } = useProductItemContext();
const [intervalCount, setIntervalCount] = useState(item.interval_count || 1);
const triggerRef = useRef<HTMLButtonElement>(null);
const handleSave = () => {
setItem({
@@ -27,20 +28,22 @@ export const CustomiseIntervalPopover = () => {
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
// className="h-8 rounded-xs min-w-7.5 max-w-7.5"
className="w-full justify-start px-2"
variant="ghost"
disabled={item.included_usage == Infinite || item.interval == null}
>
{/* <ArrowUp01 size={12} className="text-t2" /> */}
<p className="text-t3">Customise Interval</p>
</Button>
</PopoverTrigger>
<PopoverContent
// side="bottom"
align="start"
className="p-2 w-fit"
sideOffset={-1}
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
// avoidCollisions={false}
// sticky="always"
>
<div>
<FieldLabel>Interval Count</FieldLabel>

View File

@@ -1,7 +1,6 @@
import FieldLabel from "@/components/general/modal-components/FieldLabel";
import { Select } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
@@ -29,14 +28,6 @@ export const SelectCycle = () => {
});
};
// const intervalText = (interval: BillingInterval) => {
// return interval === BillingInterval.SemiAnnual
// ? "per half year"
// : interval === BillingInterval.OneOff
// ? "one off"
// : `per ${interval}`;
// };
return (
<div className="w-full">
<FieldLabel className="flex items-center gap-2">