Skip to content

Commit 7be86e8

Browse files
committed
server/product: fix eager load error triggered after attached custom fields validation error
When we validate custom fields, we remove all the existing fields, and create a savepoint. If one of the new fields is invalid, we rollback to the savepoint, which removes all the newly created fields. This behavior apparently invalidates all the eagerly loaded relationships, like prices, causing an error when we try to access them later in the request lifecycle. The fix is to first make the prices validation, and do the custom fields validation at the end.
1 parent 63d4a88 commit 7be86e8

File tree

2 files changed

+59
-17
lines changed

2 files changed

+59
-17
lines changed

server/polar/product/service.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,24 @@ async def update(
278278
) -> Product:
279279
errors: list[ValidationError] = []
280280

281+
# Validate prices
282+
existing_prices = set(product.prices)
283+
added_prices: list[ProductPrice] = []
284+
if update_schema.prices is not None:
285+
(
286+
_,
287+
existing_prices,
288+
added_prices,
289+
prices_errors,
290+
) = await self._get_validated_prices(
291+
session,
292+
update_schema.prices,
293+
product.recurring_interval,
294+
product,
295+
auth_subject,
296+
)
297+
errors.extend(prices_errors)
298+
281299
# Prevent non-legacy products from changing their recurring interval
282300
if (
283301
update_schema.recurring_interval is not None
@@ -381,23 +399,6 @@ async def update(
381399
await nested.rollback()
382400
errors.extend(attached_custom_fields_errors)
383401

384-
existing_prices = set(product.prices)
385-
added_prices: list[ProductPrice] = []
386-
if update_schema.prices is not None:
387-
(
388-
_,
389-
existing_prices,
390-
added_prices,
391-
prices_errors,
392-
) = await self._get_validated_prices(
393-
session,
394-
update_schema.prices,
395-
product.recurring_interval,
396-
product,
397-
auth_subject,
398-
)
399-
errors.extend(prices_errors)
400-
401402
if errors:
402403
raise PolarRequestValidationError(errors)
403404

server/tests/product/test_endpoints.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
ProductPriceFixed,
1414
UserOrganization,
1515
)
16+
from polar.models.custom_field import CustomFieldType
1617
from polar.postgres import AsyncSession
1718
from tests.fixtures.database import SaveFixture
1819
from tests.fixtures.random_objects import (
20+
create_custom_field,
21+
create_product,
1922
set_product_benefits,
2023
)
2124

@@ -426,6 +429,44 @@ async def test_existing_price_with_full_schema(
426429
assert isinstance(product_price, ProductPriceFixed)
427430
assert price["price_amount"] == product_price.price_amount
428431

432+
@pytest.mark.auth
433+
async def test_invalid_attached_custom_fields(
434+
self,
435+
save_fixture: SaveFixture,
436+
session: AsyncSession,
437+
client: AsyncClient,
438+
organization: Organization,
439+
user_organization: UserOrganization,
440+
) -> None:
441+
custom_field = await create_custom_field(
442+
save_fixture,
443+
type=CustomFieldType.text,
444+
slug="test-field",
445+
organization=organization,
446+
)
447+
product = await create_product(
448+
save_fixture,
449+
organization=organization,
450+
recurring_interval=None,
451+
attached_custom_fields=[
452+
(custom_field, True),
453+
],
454+
)
455+
456+
response = await client.patch(
457+
f"/v1/products/{product.id}",
458+
json={
459+
"attached_custom_fields": [
460+
{
461+
"custom_field_id": str(uuid.uuid4()),
462+
"required": False,
463+
}
464+
]
465+
},
466+
)
467+
468+
assert response.status_code == 422
469+
429470

430471
@pytest.mark.asyncio
431472
class TestUpdateProductBenefits:

0 commit comments

Comments
 (0)