Skip to content

feat(organization): support partial updates of settings groups - #13815

Open
pieterbeulque wants to merge 2 commits into
mainfrom
claude/org-settings-partial-updates-33kq82
Open

feat(organization): support partial updates of settings groups#13815
pieterbeulque wants to merge 2 commits into
mainfrom
claude/org-settings-partial-updates-33kq82

Conversation

@pieterbeulque

@pieterbeulque pieterbeulque commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Related Issue: N/A

PATCH /v1/organizations/{id} couldn't partially update the customer_email_settings, subscription_settings, or customer_portal_settings groups: callers had to send each group in full. This makes them behave like feature_settings and dispute_settings, which already support partial merges.

What

  • Added dedicated partial-update schemas — OrganizationCustomerEmailSettingsUpdate, OrganizationSubscriptionSettingsUpdate, and OrganizationCustomerPortalSettingsUpdate (with nested CustomerPortalUsageSettingsUpdate / CustomerPortalSubscriptionSettingsUpdate / CustomerPortalCustomerSettingsUpdate) — where every field is optional.
  • OrganizationUpdate now references these *Update variants instead of the full model TypedDicts.
  • The service merges each group into the organization's current settings instead of replacing it wholesale. customer_portal_settings is deep-merged so a nested group (e.g. subscription) can be changed without resending its siblings.
  • Regenerated the TypeScript client (clients/packages/client/src/v1.ts).

Why

Previously these three groups were typed as their full TypedDicts and written by wholesale assignment. Because those TypedDicts require every key, omitting any nested field returned a 422, and there was no way to toggle a single flag (e.g. turning off one customer_email notification) without re-sending the whole object. feature_settings and dispute_settings already merge partial input; this brings the remaining settings groups in line.

How

  • Each *Update schema mirrors its settings model but with all fields | None = None, so a subset validates. subscription_settings.proration_behavior keeps its public JSON-schema restriction (invoice / prorate / next_period), so the generated client surface is unchanged.
  • In OrganizationService.update, each group is merged with model_dump(mode="json", exclude_unset=True, exclude_none=True) on top of the stored value, and the two newly-handled groups are excluded from the generic update_dict so they aren't re-applied. customer_email_settings is merged onto resolve_default_customer_email_settings(...) so the stored value stays complete.
  • Full-object updates continue to work (a complete payload merges to the same result as before), so this is backwards compatible.

Testing

  • Added endpoint tests for partial customer_email_settings, subscription_settings, and deep-merged customer_portal_settings updates, plus a full-object customer_email_settings update.
  • Ran TestUpdateOrganization (20 passed) and tests/organization/test_service.py + test_schemas.py (350 passed) against a local Postgres/Redis.
  • ruff check, ruff format --check, and mypy pass on the changed files.

Note: the DB-backed suite was run against a locally-installed Postgres because the sandbox couldn't pull the Docker images; the standard CI infra will re-run the full suite.

Checklist

  • This PR addresses a single concern (one bug fix, one feature, one refactor)
  • The diff is reasonably sized and easy to review
  • New functionality is covered by tests
  • Linting and type checking pass (uv run task lint && uv run task lint_types)
  • No unrelated changes or drive-by fixes are included

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jfigrw4Jbz1MHSEMxgLBVY


Generated by Claude Code

Review in cubic

`customer_email_settings`, `subscription_settings`, and
`customer_portal_settings` on `PATCH /v1/organizations/{id}` were typed
as their full TypedDicts and applied by wholesale replacement, so a
caller had to send the complete object — omitting any nested field
returned a 422, and there was no way to toggle a single flag.

Give them the same partial-merge treatment `feature_settings` and
`dispute_settings` already have: dedicated `*Update` schemas with every
field optional, merged into the current settings in the service.
`customer_portal_settings` is deep-merged so a nested group can be
changed without resending its siblings. Full-object updates keep
working unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jfigrw4Jbz1MHSEMxgLBVY
@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orbit Ready Ready Preview Aug 18, 2026 1:40pm
polar Ready Ready Preview Aug 18, 2026 1:40pm
polar-sandbox Ready Ready Preview Aug 18, 2026 1:40pm
polar-test Ready Ready Preview Aug 18, 2026 1:40pm
polar-test-services Error Error Aug 18, 2026 1:40pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

OpenAPI Changes

Schemas

  • 🔼 CustomerPortalCustomerSettingsUpdate (added)
  • 🔼 CustomerPortalSubscriptionSettingsUpdate (added)
  • 🔼 CustomerPortalUsageSettingsUpdate (added)
  • 🔼 OrganizationCustomerEmailSettingsUpdate (added)
  • 🔼 OrganizationCustomerPortalSettingsUpdate (added)
  • 🔼 OrganizationSubscriptionSettingsUpdate (added)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/polar/organization/service.py Outdated
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Preview Environment
URL: https://pr-13815.taildbff7b.ts.net
API: https://pr-13815.taildbff7b.ts.net/v1/
Logs: backend · frontend
SHA: dfb34a18b927d0090ec29d57e9c179958e6685b3

When a legacy organization lacked an explicit
`payment_method_expiration_reminder`, its fallback is derived from
`subscription_cycled`. The partial-update merge resolved defaults against
the *old* stored settings before applying the update, so changing
`subscription_cycled` in the same PATCH left the persisted reminder
following the old cycle.

Merge the partial update first, then resolve defaults, so the reminder
fallback follows the `subscription_cycled` value set in the same request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jfigrw4Jbz1MHSEMxgLBVY

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Auto-approved: Adds optional-field update schemas and merge logic so PATCH can partially update organization settings groups, with new endpoint tests pinning the merge behavior and backwards compatibility. Diff is scoped and self-contained, no security, data, or operational tradeoffs introduced.

Re-trigger cubic



class OrganizationCustomerEmailSettingsUpdate(Schema):
"""Partial update for customer email settings.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those doctoring will make their way to the OpenAPI, SDK and docs, so they should be a bit less technical or dropped entirely.

}
# Resolve defaults after merging so a legacy organization that lacks
# `payment_method_expiration_reminder` derives its fallback from the
# `subscription_cycled` value set in this same request, not the old one.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's why when we add a new setting, we shouldn't be lazy and make a proper migration. Otherwise, we need this kind of workarounds.

@cubic-dev Please remember that for the next time you see a new or removed organization setting.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@frankie567 Saved as a learning. You can edit it anytime in Settings → AI review → Learnings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe I'll clean up the data first so that we don't need to do this...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants