Repository navigation
fix(billing): keep deleted groups out of subscription plan coverage - #1990
Merged
Merged
Conversation
The plan editor loaded the stored group selection and submitted it unchanged, while only rendering checkboxes for groups that still exist. A group deleted after the plan was saved therefore stayed in the request without any control that could remove it, so the plan kept naming a group that no longer resolves. Loading the group choices now prunes selected identifiers missing from the loaded list together with their rate overrides, and reports how many were dropped. A disabled group remains selectable, and a failed load leaves the draft untouched so a temporary error cannot erase the selection.
A plan keeps the group identifiers recorded when it was saved, so a group deleted afterwards still appeared in a subscriber's plan card as a bare identifier with no name. The lookup result was discarded, leaving an identifier-only entry for every unresolved group. Enrichment now removes an entry when storage reports that the group does not exist, and keeps the identifier-only entry for a nil row or another read failure, where the group's existence is undetermined.
Group deletion removes a group's permissions and provider memberships, but plans keep the identifiers they recorded. A later plan save then resubmitted a selection pointing at a group that no longer resolves, and nothing rejected it because plan writes only deduplicate and drop nonpositive identifiers. Plan writes now confirm a declared group selection against stored groups before persistence. CreatePlan checks the selection it normalizes, and UpdatePlan checks the selection it is about to store, including a legacy leading identifier. An update that leaves group_ids out keeps the recorded selection and runs no lookup, so an unrelated edit still works on a plan that already names a deleted group.
The plan and subscription documents described what a nonempty group selection means without saying what happens when a selected group is deleted. Add the write-time check, the display behaviour for a confirmed missing group, and the editor repair, and list the plan endpoints with their GROUP_NOT_FOUND response.
Call PlanStore directly and require a foreign-key error before checking that the earlier plan insert was rolled back.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
套餐记录的分组可以在分组被删除之后继续保留,订阅页于是把它显示成
#15这样的裸编号,套餐里也一直占着一个永远匹配不到的分组位。实际触发场景是一个 Max 套餐,group_ids里仍然列着已经删除的分组 15,同一份列表里的其他八个分组都正常显示名称。原因在四个环节:
mixins.SoftDeleteMixin),行还留在表里,subscription_plan_groups.group_id上的ON DELETE CASCADE不会触发。GroupStore.DeleteCascade清理分组授权和提供商关联,套餐侧没有对应处理。NormalizePlanGroupIDs只过滤非正数和重复值,套餐写入接受任何正整数。EnrichSubscriptionPlanGroups对无法利用的查询结果保留只有 ID 的条目,SubscriptionsView.vue把它渲染成#{id}。这个编号从未带来权限,额度判定读的是同一份列表,没有分组与之匹配。它占着位置,还让订阅者看到一个没有信息量的标签。
修改
PlanEditDialog.vue):分组列表加载成功后,不在列表中的编号连同倍率覆盖一起从草稿移除,并提示移除了几个。停用的分组仍可勾选;加载失败时草稿保持不变;请求代次守卫让迟到的旧响应无法用过期列表裁剪新草稿。EnrichSubscriptionPlanGroups):只在存储明确报告分组不存在时移除条目。返回空行、读取器缺失或读取失败都保留只有 ID 的条目,因为这些结果说明存在性仍未确定。Plans.CreatePlan、Plans.UpdatePlan):提交的分组选择在落库前核对现存分组,指向已删除或不存在的分组返回GROUP_NOT_FOUND(404)。billing定义PlanGroupValidator,由app适配到routing.ValidateGroupIDs;billing不能直接 importrouting,否则会经provider形成 import cycle。docs/domains/payments_and_entitlements.md记录规则与展示行为,docs/interfaces/http_api.md补充两个套餐接口的GROUP_NOT_FOUND响应。取舍
group_ids时保留原有分组选择,也不做分组查询,已记录旧编号的套餐仍能改价格和额度,编辑页下一次保存时顺带修复。routing反向依赖套餐存储。验证
make test-go:后端全量测试,含tests/integrationpnpm run test:run:400 个文件、3101 个测试通过make lint-go:架构规则、文件布局检查、golangci-lint(0 issues)make fmt-check、前端typecheck、注释与 UI token 检查通过新增覆盖:删除的分组连同倍率覆盖被移除并提示;停用分组及其覆盖保留;加载失败保留原有选择;迟到的旧响应不裁剪新草稿;展示层移除确认不存在的分组、保留不可读或空行的分组;创建和更新拒绝指向已删除分组的列表(含 legacy
group_id合并),省略分组时不做查询。