Skip to content

Commit 2fd8dd9

Browse files
committed
fix(payments): improve price validation and handling in product dialogs
- Added validation to ensure at least one price is provided when saving a product. - Enhanced error handling to clear price-related errors when prices are updated. - Updated price validation schema to allow for null values. - Removed unnecessary price reset logic in the product prices section. These changes aim to improve user experience by enforcing price requirements and providing clearer feedback in the product dialog.
1 parent 9cf9f49 commit 2fd8dd9

5 files changed

Lines changed: 33 additions & 4 deletions

File tree

apps/backend/src/app/api/latest/payments/products/[customer_type]/[customer_id]/switch/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ export const POST = createSmartRouteHandler({
116116
Object.values(subMap).filter(s => isActiveSubscription(s)).map(s => s.productId ?? "__null__")
117117
);
118118
const hasOtpInProductLine = Object.entries(ownedProducts).some(
119-
([productId, p]) => p.productLineId === fromProduct.productLineId
119+
([productId, p]) => productId !== body.from_product_id
120+
&& p.productLineId === fromProduct.productLineId
120121
&& p.quantity > 0
121122
&& !activeSubProductIds.has(productId)
122123
);

apps/backend/src/app/api/latest/payments/purchases/validate-code/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export const POST = createSmartRouteHandler({
8080
const productLineId = Object.keys(productLines).find((g) => product.productLineId === g);
8181
let conflictingProductLineProducts: { product_id: string, display_name: string }[] = [];
8282
if (productLineId) {
83-
const isSubscribable = Object.values(product.prices).some((p: any) => p && p.interval);
83+
const productPrices = product.prices;
84+
const isSubscribable = productPrices !== "include-by-default" && Object.values(productPrices).some((p) => p != null && p.interval != null);
8485
if (isSubscribable) {
8586
const addOnBaseProductIds = product.isAddOnTo ? new Set(Object.keys(product.isAddOnTo)) : new Set<string>();
8687
conflictingProductLineProducts = Object.entries(ownedProducts)

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/[productId]/page-client.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,6 @@ function ProductPricesSection({ productId, prices, onPricesChange, inline = fals
864864
const hasNoPrices = priceEntries.length === 0;
865865

866866
const handleMakePaid = () => {
867-
onPricesChange({});
868867
openAddDialog();
869868
};
870869

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/product-dialog.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ export function ProductDialog({
161161
};
162162

163163
const handleSave = async () => {
164+
if (Object.keys(prices).length === 0) {
165+
setErrors({ prices: "At least one price is required" });
166+
return;
167+
}
168+
164169
const product: Product = {
165170
displayName,
166171
customerType,
@@ -610,11 +615,24 @@ export function ProductDialog({
610615
<ListSection title="Prices">
611616
<PricingSection
612617
prices={prices}
613-
onPricesChange={setPrices}
618+
onPricesChange={(newPrices) => {
619+
setPrices(newPrices);
620+
if (errors.prices && Object.keys(newPrices).length > 0) {
621+
setErrors(prev => {
622+
const { prices: _, ...rest } = prev;
623+
return rest;
624+
});
625+
}
626+
}}
614627
variant="dialog"
615628
/>
616629
</ListSection>
617630
</div>
631+
{errors.prices ? (
632+
<Typography type="p" className="text-destructive text-sm">
633+
{errors.prices}
634+
</Typography>
635+
) : null}
618636
</div>
619637
</div>
620638
</StepperPage>

apps/e2e/tests/backend/endpoints/api/v1/payments/block-new-purchases.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ it("should block switch endpoint when blockNewPurchases is enabled", async ({ ex
233233

234234
const { userId } = await Auth.fastSignUp();
235235

236+
// Grant planA ownership via server so the switch would otherwise have a valid
237+
// source subscription — without this, the endpoint could reject for an unrelated
238+
// reason and the test would pass by accident if the block check ever moved.
239+
const grantResponse = await niceBackendFetch(`/api/latest/payments/products/user/${userId}`, {
240+
method: "POST",
241+
accessType: "server",
242+
body: { product_id: "planA" },
243+
});
244+
expect(grantResponse.status).toBe(200);
245+
236246
const switchResponse = await niceBackendFetch(`/api/latest/payments/products/user/${userId}/switch`, {
237247
method: "POST",
238248
accessType: "client",

0 commit comments

Comments
 (0)