fix: customise interval UI, carry existing usage diff intervals
This commit is contained in:
@@ -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' \
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user