Skip to content

test(api): added v3 product catalog e2e tests#4201

Merged
rolosp merged 4 commits intomainfrom
chore/productcatalog-v3-e2e
Apr 23, 2026
Merged

test(api): added v3 product catalog e2e tests#4201
rolosp merged 4 commits intomainfrom
chore/productcatalog-v3-e2e

Conversation

@rolosp
Copy link
Copy Markdown
Collaborator

@rolosp rolosp commented Apr 21, 2026

Overview

  • api/v3/handlers/plans/convert.go: removed early currency validation in
    FromAPICreatePlanRequest; the service layer's PlanMeta.Validate() already produces
    a structured ValidationIssue with the correct HTTP status and validationErrors
    extensions — behaviour from the client's perspective is unchanged (still 400), but
    the error shape is now consistent with all other validation errors
  • openmeter/productcatalog/planaddon/service/planaddon.go: UpdatePlanAddon now
    validates the post-merge state before persisting — builds a PlanAddon reflecting
    the patched fields (applying nil = keep-existing semantics for
    FromPlanPhase/Metadata/Annotations, nil = clear for MaxQuantity) and calls
    PlanAddon.Validate(), returning a structured GenericValidationError on failure

e2e tests (e2e/)

  • v3helpers_test.go: shared v3Client struct, typed request/response helpers,
    fixture builders (validAddonRequest, validFlatRateCard, validUnitRateCard,
    validGraduatedRateCard, etc.), and a withPageSize option
  • addons_v3_test.go: full addon lifecycle (create → get → list → update → publish
    → archive → delete → soft-delete) plus mixed rate-card round-trip and
    versioning/auto-archive tests
  • plans_v3_test.go: plan lifecycle, versioning, and auto-archive tests
  • planaddons_v3_test.go: plan-addon attachment, status-matrix validation, and
    lifecycle tests

Skill

Added .agents/skills/e2e/SKILL.md documenting the v3 e2e test patterns for future
agents.

Summary by CodeRabbit

  • Documentation

    • Added comprehensive E2E testing guide with v1/v3 patterns and best practices.
    • Clarified Plan-Addon MaxQuantity semantics (nil = unlimited; positive integer required when provided).
    • Updated generated API documentation payload.
  • Tests

    • Added extensive V3 E2E suites covering plans, addons, plan-addon lifecycles, validation matrices, versioning, and edge cases.
    • Introduced V3 HTTP client and test helpers for stable live-server tests.
  • Bug Fixes

    • Enforced plan-addon compatibility validation during updates.
    • Strengthened currency validation coverage for plan creation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds comprehensive v3 end-to-end tests and a lightweight v3 test client, documents e2e practices, defers plan currency validation from handler to domain validation, and adds post-merge compatibility validation when updating plan‑addon assignments.

Changes

Cohort / File(s) Summary
Test Documentation
/.agents/skills/e2e/SKILL.md
New e2e skill guide describing v1/v3 test styles, run patterns (-tags=dynamic, OPENMETER_ADDRESS gating, RUN_SLOW_TESTS), conventions (unique keys, pagination), error shapes, timing, and assertion patterns.
Handler Conversion
api/v3/handlers/plans/convert.go, api/v3/handlers/plans/convert_test.go
Removed eager currency validation in handler conversion; handler now assigns currency code and defers semantic validation downstream. Corresponding negative test for invalid currency removed.
E2E HTTP Client & Helpers
e2e/v3helpers_test.go
Introduces v3Client with per-request timeouts, RFC7807 problem parsing, typed decode helpers, pagination builders, deterministic fixture helpers (uniqueKey), and assertion helpers for validation/problem shapes.
E2E Plan Tests
e2e/plans_v3_test.go
New plan lifecycle and validation tests (create/update/publish/archive/delete), phase/rate-card validations, versioning/auto-archive, publish-time validations, and invalid-currency test.
E2E Addon Tests
e2e/addons_v3_test.go
New addon lifecycle and validation tests: draft→publish→archive→delete flows, rate-card round-trip checks, versioning/auto-archive, feature-reference validation, and compatibility matrices.
E2E Plan-Addon Tests
e2e/planaddons_v3_test.go
New attach/detach/publish tests: lifecycle, plan/addon status matrices, instance-type/max-quantity rules, detach restrictions, duplicate-attachment rejection, and helper setup functions.
Service Validation
openmeter/productcatalog/planaddon/service/planaddon.go
Added post-merge compatibility validation in UpdatePlanAddon: merges prospective values, runs pa.Validate(), and returns structured validation errors instead of persisting invalid assignments.
API Model Docs & Generated Spec
api/v3/api.gen.go, api/spec/.../planaddon.tsp
Clarified maxQuantity/MaxQuantity semantics (nil = unlimited, positive = cap, zero/negative invalid, single-instance must omit) in model comments and regenerated embedded swagger spec.
Domain Messages & Comments
openmeter/productcatalog/errors.go, openmeter/productcatalog/planaddon.go
Updated human-readable validation message and expanded MaxQuantity comment to match new semantics.

Sequence Diagram(s)

sequenceDiagram
  participant Tester as Test (v3Client)
  participant API as API v3 Server
  participant Service as ProductCatalog Service
  participant DB as Database
  Tester->>API: POST /api/v3/... (create plan/addon/attach)
  API->>Service: convert & validate request
  Service->>DB: persist draft resource
  DB-->>Service: OK
  Service-->>API: 201 Draft response
  API-->>Tester: created (Draft)
  Tester->>API: POST /api/v3/.../publish
  API->>Service: publish -> run domain validations (including merged plan-addon checks)
  Service->>DB: update status / set EffectiveFrom or EffectiveTo
  DB-->>Service: OK
  Service-->>API: 200/204 (Published/Updated)
  API-->>Tester: publish response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

area/billing

Suggested reviewers

  • tothandras
  • chrisgacsal
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: adding v3 product catalog end-to-end tests across multiple test files and helpers.
Docstring Coverage ✅ Passed Docstring coverage is 92.86% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/productcatalog-v3-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rolosp rolosp added the release-note/misc Miscellaneous changes label Apr 22, 2026
@rolosp rolosp marked this pull request as ready for review April 22, 2026 08:22
@rolosp rolosp requested a review from a team as a code owner April 22, 2026 08:22
@rolosp rolosp force-pushed the chore/productcatalog-v3-e2e branch from fb0cf2f to d81a4ae Compare April 22, 2026 08:22
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (8)
e2e/planaddons_v3_test.go (3)

216-300: Great matrix — one small coverage gap worth noting.

The multiple + max_quantity=0 → schema minimum row confirms the TypeSpec binder catches <1 before the domain validator runs, which is exactly the right shape. Nice.

One gap: negative values (e.g. max_quantity=-1). If the schema minimum is 1, negatives should hit the same minimum rule, but there's a non-zero chance they decode/validate differently (signed int parsing edge, or the binder interpreting the sign). A single extra row would close that door:

♻️ Optional extra row
 		{
 			// Caught by the TypeSpec binder (minimum: 1), not the domain
 			// validator — the schema layer fires first for max_quantity<1.
 			name:           "multiple + max_quantity=0 → 400 schema minimum",
 			instanceType:   apiv3.AddonInstanceTypeMultiple,
 			maxQuantity:    lo.ToPtr(0),
 			expectedStatus: http.StatusBadRequest,
 			expectedRule:   "minimum",
 		},
+		{
+			name:           "multiple + max_quantity=-1 → 400 schema minimum",
+			instanceType:   apiv3.AddonInstanceTypeMultiple,
+			maxQuantity:    lo.ToPtr(-1),
+			expectedStatus: http.StatusBadRequest,
+			expectedRule:   "minimum",
+		},

Non-blocking; ship it either way.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 216 - 300, Add one more test case to
TestV3PlanAddonInstanceTypeMaxQuantityMatrix’s cases table: a row for
instanceType apiv3.AddonInstanceTypeMultiple with maxQuantity set to
lo.ToPtr(-1) and expectedStatus http.StatusBadRequest with expectedRule
"minimum". Place it near the existing "multiple + max_quantity=0 → 400 schema
minimum" case in the cases slice so the binder/schema-layer behavior for
negative values is covered by the same assertion paths
(assertInvalidParameterRule / expectedRule).

368-386: Nice test — assertion could be a touch stricter.

The test confirms publish plan against archived addon → 400, which is the important bit. Since we're here, consider also asserting on the specific problem code or detail so a future refactor that accidentally returns 400 for the wrong reason (e.g., transient 400 from a different validator) still fails meaningfully:

♻️ Optional: pin the rejection reason
 	status, _, problem = c.PublishPlan(planID)
 	assert.Equal(t, http.StatusBadRequest, status, "publish against archived addon should reject: %+v", problem)
 	require.NotNil(t, problem)
+	// Pin the reason so a future 400-for-a-different-reason doesn't slip by.
+	// assertValidationCode(t, problem, "plan_addon_incompatible_status")
+	// or assertProblemDetail(t, problem, "<expected substring>")

Per the skill doc's own "write it, run it, inspect the problem" guidance — once you've seen the response shape on a real run, uncommenting one of these would tighten this up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 368 - 386, In
TestV3PlanAddonPublishAgainstArchivedAddon, tighten the failure check by
asserting the specific rejection reason from the returned problem object (rather
than only status 400) — inspect the real test run to see the problem structure
and add an assertion against the appropriate field on problem (for
example/assert-style: require.Equal(t, "<expected_problem_code>", problem.Code)
or require.Contains(t, problem.Detail, "archived") or assert.Contains(t,
problem.Message, "archived")), keeping the existing status and non-nil checks;
update the assertions around the PublishPlan call in this test to include that
extra check.

327-345: Pinning observed behavior is fine, but worth a breadcrumb for future-you.

The "delete from deleted plan returns 400 with plan is deleted detail" is a behavior choice the server could reasonably flip to 404 later. The comment on line 328-329 acknowledges that. As a belt-and-suspenders move, consider accepting both shapes so this test doesn't become a tripwire the day someone tidies the handler:

♻️ Optional: accept either pinned behavior
-		status, problem = c.DetachAddon(planID, planAddonID)
-		assert.Equal(t, http.StatusBadRequest, status, "detach from deleted: %+v", problem)
-		assertProblemDetail(t, problem, "plan is deleted")
+		status, problem = c.DetachAddon(planID, planAddonID)
+		assert.Contains(t, []int{http.StatusBadRequest, http.StatusNotFound}, status,
+			"detach from deleted: %+v", problem)
+		if status == http.StatusBadRequest {
+			assertProblemDetail(t, problem, "plan is deleted")
+		}

Totally optional — sometimes pinning is the point of the test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 327 - 345, The test currently
strictly expects a 400 with detail "plan is deleted" when calling c.DetachAddon
in the t.Run "detach from a deleted plan → 400 'plan is deleted'"; relax it to
accept either the pinned behavior or a 404 by checking the returned status from
c.DetachAddon and asserting: if status == http.StatusBadRequest then call
assertProblemDetail(t, problem, "plan is deleted"), otherwise assert status ==
http.StatusNotFound (and optionally assert problem is nil or has a not-found
detail); keep the existing setupAttachedPlanAddon,
PublishPlan/ArchivePlan/DeletePlan sequence and the test name intact.
e2e/plans_v3_test.go (1)

386-403: Table readability nit — the inline isLast comments are a head-scratcher.

The cases are correct (first phase of a 2-phase slice is the "non-last", the last is the "last"), but the inline comments describe what isLast=true/false means to validPlanPhase, not what the test is asserting. So nonLast := validPlanPhase("non_last", true /* isLast — no duration */) reads as "this IS last, has no duration" while the variable name + test name say it's a non-last phase. It clicks once you trace through, but it slowed me down.

A small rename of the local vars or flipping the comment to describe the resulting defect would help:

♻️ One shape for clearer reading
 		{
 			name: "non-last phase missing duration → plan_has_non_last_phase_with_no_duration",
 			mutate: func(r *apiv3.CreatePlanRequest) {
-				nonLast := validPlanPhase("non_last", true /* isLast — no duration */)
-				last := validPlanPhase("last", true /* isLast */)
-				r.Phases = []apiv3.BillingPlanPhase{nonLast, last}
+				// Both phases built with isLast=true → neither has a duration.
+				// The first one is positionally non-last, so it's the defect.
+				firstNoDuration := validPlanPhase("non_last", true)
+				last := validPlanPhase("last", true)
+				r.Phases = []apiv3.BillingPlanPhase{firstNoDuration, last}
 			},
 			code: "plan_has_non_last_phase_with_no_duration",
 		},
 		{
 			name: "last phase with a duration → plan_has_last_phase_with_duration",
 			mutate: func(r *apiv3.CreatePlanRequest) {
-				first := validPlanPhase("first", false /* isLast */)
-				last := validPlanPhase("last", false /* bounded — should be open */)
+				// Both phases built with isLast=false → both carry a duration.
+				// The second is positionally last, so its duration is the defect.
+				first := validPlanPhase("first", false)
+				lastWithDuration := validPlanPhase("last", false)
-				r.Phases = []apiv3.BillingPlanPhase{first, last}
+				r.Phases = []apiv3.BillingPlanPhase{first, lastWithDuration}
 			},
 			code: "plan_has_last_phase_with_duration",
 		},

Totally optional — just a kindness to the next reader.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/plans_v3_test.go` around lines 386 - 403, The inline comments next to
calls to validPlanPhase in the test cases are describing the meaning of the
boolean parameter rather than the intended test scenario, which is confusing
(e.g., nonLast := validPlanPhase("non_last", true /* isLast — no duration */)
reads as “is last” while the test intends a non-last phase). Fix by renaming the
local variables or updating the inline comments so they describe the resulting
defect being tested (e.g., mark the boolean comment to indicate that passing
true produces a phase with no duration, or rename nonLast→phaseWithNoDuration
and last→openLastPhase), keeping references to validPlanPhase and the test case
names (plan_has_non_last_phase_with_no_duration,
plan_has_last_phase_with_duration) so intent is clear.
e2e/addons_v3_test.go (1)

306-318: Future-proofing nit on the matrix loop.

require.NotNil(t, addon) runs unconditionally, which is fine today because every row expects 201. But the table shape invites future rows with expectedStatus=http.StatusBadRequest (there's even a comment above about dropped rate-card compatibility cases that should be re-enabled), and any of those will panic here instead of failing cleanly on the status assertion.

Small safety net:

♻️ Gate the nil check on the expected status
 			status, addon, problem := c.CreateAddon(body)
 			assert.Equal(t, tc.expectedStatus, status, "problem: %+v", problem)
-			require.NotNil(t, addon)
+			if tc.expectedStatus == http.StatusCreated {
+				require.NotNil(t, addon)
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/addons_v3_test.go` around lines 306 - 318, The test unconditionally calls
require.NotNil(t, addon) after c.CreateAddon(body), which will panic for cases
expecting a non-201 status; change the assertion to only require.NotNil when the
expected status is http.StatusCreated (201). Locate the table-driven loop that
calls newV3Client(), validAddonRequest("instance_type_price"), sets
body.InstanceType and body.RateCards, then calls c.CreateAddon(body); after
asserting status equals tc.expectedStatus, guard the require.NotNil(t, addon)
behind a conditional like if tc.expectedStatus == http.StatusCreated (or 201) so
tests expecting errors fail cleanly on the status assertion instead of
panicking.
e2e/v3helpers_test.go (2)

341-422: Fixture builders panic on FromBillingPrice* errors — consider a shared helper.

The three rate-card builders (validFlatRateCard, validUnitRateCard, validGraduatedRateCard) each hand-roll the BillingPrice{} + FromBillingPriceXxx + panic(err) dance. For hardcoded inputs it's fine (they'll never realistically fail), but a tiny helper would tighten things and reduce the repeated boilerplate if you add more shapes later:

♻️ Optional: centralize the "build-or-panic" price construction
func mustPrice(fn func(*apiv3.BillingPrice) error) apiv3.BillingPrice {
    p := apiv3.BillingPrice{}
    if err := fn(&p); err != nil {
        panic(fmt.Errorf("build fixture price: %w", err))
    }
    return p
}

// usage:
price := mustPrice(func(p *apiv3.BillingPrice) error {
    return p.FromBillingPriceFlat(apiv3.BillingPriceFlat{
        Type: apiv3.BillingPriceFlatTypeFlat, Amount: "10",
    })
})

Very much a nice-to-have — no correctness concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/v3helpers_test.go` around lines 341 - 422, The three fixture builders
(validFlatRateCard, validUnitRateCard, validGraduatedRateCard) duplicate the
BillingPrice + FromBillingPrice* + panic(err) pattern; add a small helper like
mustPrice(fn func(*apiv3.BillingPrice) error) apiv3.BillingPrice that constructs
a BillingPrice, calls the provided FromBillingPrice* function
(FromBillingPriceFlat, FromBillingPriceUnit, FromBillingPriceGraduated) and
panics with a wrapped error (fmt.Errorf("build fixture price: %w", err)) on
failure, then replace the inline construction in each builder with a call to
mustPrice to remove the boilerplate.

95-110: Minor: swallowed unmarshal error on validationErrors.

If extensions.validationErrors is present but malformed (e.g., a server bug returns it as a single object instead of an array), this silently returns nil and the subsequent assertValidationCode will fail with the less-informative "no validation errors in problem response" instead of telling you "validationErrors existed but didn't decode as []v3ValidationError". Low-probability but free to surface:

♻️ Surface decode errors via t.Helper for easier debugging
-	b, err := json.Marshal(raw)
-	if err != nil {
-		return nil
-	}
-	var out []v3ValidationError
-	_ = json.Unmarshal(b, &out)
-	return out
+	b, err := json.Marshal(raw)
+	if err != nil {
+		return nil
+	}
+	var out []v3ValidationError
+	if err := json.Unmarshal(b, &out); err != nil {
+		return nil
+	}
+	return out

Totally optional — the current behavior won't hide real bugs, just makes the failure message less precise.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/v3helpers_test.go` around lines 95 - 110, The current
v3Problem.ValidationErrors silently swallows json.Marshal/json.Unmarshal errors;
update it to surface decode failures in tests by changing the method to accept t
*testing.T (e.g., func (p *v3Problem) ValidationErrors(t *testing.T)
[]v3ValidationError), call t.Helper(), and call t.Fatalf with a clear message if
Marshal/Unmarshal returns an error so malformed extensions.validationErrors
produce an actionable test failure; update all test callers to pass their
*testing.T accordingly.
.agents/skills/e2e/SKILL.md (1)

88-98: Tiny heads-up on the illustrative snippet.

The api.UpsertSubjectJSONRequestBody{ api.SubjectUpsert{Key: "customer-1"} } literal reads oddly — oapi-codegen usually emits UpsertSubjectJSONRequestBody as a type alias for SubjectUpsert, so either the outer wrapper is redundant or (if it's a struct) the inner field needs a name. Since this block is the "copy me as a template" example in the skill doc, a compiling snippet would save the next reader a detour through api.gen.go.

Not blocking — just a paper-cut for future-you.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agents/skills/e2e/SKILL.md around lines 88 - 98, The example TestIngest
snippet uses a nested composite literal that won't compile; replace the
inner/outer mismatch when calling UpsertSubjectWithResponse. Locate TestIngest
and change the request arg from api.UpsertSubjectJSONRequestBody{
api.SubjectUpsert{Key: "customer-1"} } to the correct literal form (either
api.UpsertSubjectJSONRequestBody{Key: "customer-1"} if
UpsertSubjectJSONRequestBody is a struct, or simply api.SubjectUpsert{Key:
"customer-1"} if UpsertSubjectJSONRequestBody is a type alias) so the call to
UpsertSubjectWithResponse compiles.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.agents/skills/e2e/SKILL.md:
- Around line 88-98: The example TestIngest snippet uses a nested composite
literal that won't compile; replace the inner/outer mismatch when calling
UpsertSubjectWithResponse. Locate TestIngest and change the request arg from
api.UpsertSubjectJSONRequestBody{ api.SubjectUpsert{Key: "customer-1"} } to the
correct literal form (either api.UpsertSubjectJSONRequestBody{Key: "customer-1"}
if UpsertSubjectJSONRequestBody is a struct, or simply api.SubjectUpsert{Key:
"customer-1"} if UpsertSubjectJSONRequestBody is a type alias) so the call to
UpsertSubjectWithResponse compiles.

In `@e2e/addons_v3_test.go`:
- Around line 306-318: The test unconditionally calls require.NotNil(t, addon)
after c.CreateAddon(body), which will panic for cases expecting a non-201
status; change the assertion to only require.NotNil when the expected status is
http.StatusCreated (201). Locate the table-driven loop that calls newV3Client(),
validAddonRequest("instance_type_price"), sets body.InstanceType and
body.RateCards, then calls c.CreateAddon(body); after asserting status equals
tc.expectedStatus, guard the require.NotNil(t, addon) behind a conditional like
if tc.expectedStatus == http.StatusCreated (or 201) so tests expecting errors
fail cleanly on the status assertion instead of panicking.

In `@e2e/planaddons_v3_test.go`:
- Around line 216-300: Add one more test case to
TestV3PlanAddonInstanceTypeMaxQuantityMatrix’s cases table: a row for
instanceType apiv3.AddonInstanceTypeMultiple with maxQuantity set to
lo.ToPtr(-1) and expectedStatus http.StatusBadRequest with expectedRule
"minimum". Place it near the existing "multiple + max_quantity=0 → 400 schema
minimum" case in the cases slice so the binder/schema-layer behavior for
negative values is covered by the same assertion paths
(assertInvalidParameterRule / expectedRule).
- Around line 368-386: In TestV3PlanAddonPublishAgainstArchivedAddon, tighten
the failure check by asserting the specific rejection reason from the returned
problem object (rather than only status 400) — inspect the real test run to see
the problem structure and add an assertion against the appropriate field on
problem (for example/assert-style: require.Equal(t, "<expected_problem_code>",
problem.Code) or require.Contains(t, problem.Detail, "archived") or
assert.Contains(t, problem.Message, "archived")), keeping the existing status
and non-nil checks; update the assertions around the PublishPlan call in this
test to include that extra check.
- Around line 327-345: The test currently strictly expects a 400 with detail
"plan is deleted" when calling c.DetachAddon in the t.Run "detach from a deleted
plan → 400 'plan is deleted'"; relax it to accept either the pinned behavior or
a 404 by checking the returned status from c.DetachAddon and asserting: if
status == http.StatusBadRequest then call assertProblemDetail(t, problem, "plan
is deleted"), otherwise assert status == http.StatusNotFound (and optionally
assert problem is nil or has a not-found detail); keep the existing
setupAttachedPlanAddon, PublishPlan/ArchivePlan/DeletePlan sequence and the test
name intact.

In `@e2e/plans_v3_test.go`:
- Around line 386-403: The inline comments next to calls to validPlanPhase in
the test cases are describing the meaning of the boolean parameter rather than
the intended test scenario, which is confusing (e.g., nonLast :=
validPlanPhase("non_last", true /* isLast — no duration */) reads as “is last”
while the test intends a non-last phase). Fix by renaming the local variables or
updating the inline comments so they describe the resulting defect being tested
(e.g., mark the boolean comment to indicate that passing true produces a phase
with no duration, or rename nonLast→phaseWithNoDuration and last→openLastPhase),
keeping references to validPlanPhase and the test case names
(plan_has_non_last_phase_with_no_duration, plan_has_last_phase_with_duration) so
intent is clear.

In `@e2e/v3helpers_test.go`:
- Around line 341-422: The three fixture builders (validFlatRateCard,
validUnitRateCard, validGraduatedRateCard) duplicate the BillingPrice +
FromBillingPrice* + panic(err) pattern; add a small helper like mustPrice(fn
func(*apiv3.BillingPrice) error) apiv3.BillingPrice that constructs a
BillingPrice, calls the provided FromBillingPrice* function
(FromBillingPriceFlat, FromBillingPriceUnit, FromBillingPriceGraduated) and
panics with a wrapped error (fmt.Errorf("build fixture price: %w", err)) on
failure, then replace the inline construction in each builder with a call to
mustPrice to remove the boilerplate.
- Around line 95-110: The current v3Problem.ValidationErrors silently swallows
json.Marshal/json.Unmarshal errors; update it to surface decode failures in
tests by changing the method to accept t *testing.T (e.g., func (p *v3Problem)
ValidationErrors(t *testing.T) []v3ValidationError), call t.Helper(), and call
t.Fatalf with a clear message if Marshal/Unmarshal returns an error so malformed
extensions.validationErrors produce an actionable test failure; update all test
callers to pass their *testing.T accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a0f5f711-a1c2-4660-9f2a-33dbdce87292

📥 Commits

Reviewing files that changed from the base of the PR and between 605f03f and d81a4ae.

📒 Files selected for processing (9)
  • .agents/skills/e2e/SKILL.md
  • api/v3/handlers/plans/convert.go
  • api/v3/handlers/plans/convert_test.go
  • e2e/addons_v3_test.go
  • e2e/planaddons_v3_test.go
  • e2e/plans_v3_test.go
  • e2e/v3helpers_test.go
  • openmeter/productcatalog/planaddon.go
  • openmeter/productcatalog/planaddon/service/planaddon.go
💤 Files with no reviewable changes (1)
  • api/v3/handlers/plans/convert_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
e2e/planaddons_v3_test.go (1)

366-376: Consider pinning the active-plan detach failure reason.

The detach from an active plan → 400 case only checks the status code. Since 400 can come from many paths, a quick assertProblemDetail (or assertValidationCode) tying this to the actual "plan is active/not draft" reason would make regressions easier to spot — similar to how the deleted-plan case is pinned at line 395. Totally optional, just a nice-to-have for symmetry.

💡 Suggested tweak
 	status, problem = c.DetachAddon(planID, planAddonID)
 	assert.Equal(t, http.StatusBadRequest, status, "detach from active should reject: %+v", problem)
+	require.NotNil(t, problem)
+	assert.NotEmpty(t, problem.Detail, "expected a reason in problem.Detail")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 366 - 376, The test "detach from an
active plan → 400" currently only asserts the HTTP 400 status; update it to also
assert the specific failure reason by calling assertProblemDetail or
assertValidationCode after c.DetachAddon to verify the error maps to "plan is
active" (or the project's validation code for non-draft plans), using the same
planID/planAddonID from setupAttachedPlanAddon and the response/problem returned
by c.DetachAddon so the test pins the rejection to the active-plan-not-draft
condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/planaddons_v3_test.go`:
- Around line 366-376: The test "detach from an active plan → 400" currently
only asserts the HTTP 400 status; update it to also assert the specific failure
reason by calling assertProblemDetail or assertValidationCode after
c.DetachAddon to verify the error maps to "plan is active" (or the project's
validation code for non-draft plans), using the same planID/planAddonID from
setupAttachedPlanAddon and the response/problem returned by c.DetachAddon so the
test pins the rejection to the active-plan-not-draft condition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ee3a620-7072-4e28-97ad-fbf619321a2a

📥 Commits

Reviewing files that changed from the base of the PR and between d81a4ae and 9722196.

📒 Files selected for processing (2)
  • e2e/planaddons_v3_test.go
  • openmeter/productcatalog/planaddon/service/planaddon.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/productcatalog/planaddon/service/planaddon.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
openmeter/productcatalog/planaddon/service/planaddon.go (2)

376-376: Stale log message.

logger.Debug("validating plan add-on assignment") sits right after the validate block, just before the adapter write. Reads a bit confusing — probably wants to be "persisting plan add-on assignment" (or moved above line 325 next to the real validation step).

🪵 Tiny tweak
-		logger.Debug("validating plan add-on assignment")
+		logger.Debug("persisting plan add-on assignment")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/planaddon/service/planaddon.go` at line 376, The log
message logger.Debug("validating plan add-on assignment") is stale because it
appears after the validation block and just before the adapter write; update it
to accurately reflect the action (e.g., logger.Debug("persisting plan add-on
assignment")) or move the existing debug call up next to the actual validation
step in the function that performs validation and adapter write (refer to the
validate block and the adapter write call in planaddon.go around the function
handling plan add-on assignment) so logs match the real operation.

325-370: Nice catch pulling validation in here — one mild hazard to flag.

The patch-merge mirrors the adapter's SetOrClearMaxQuantity / nil-skip semantics, and the heads-up comment calls out the coupling nicely. Two small things worth thinking about:

  • This is now a silent contract between this block and planaddon/adapter/planaddon.go UpdatePlanAddon. If someone later changes the adapter to (say) stop clearing MaxQuantity when params.MaxQuantity is nil, or treats Metadata == nil differently, validation and persistence will disagree and we could either reject valid updates or admit invalid ones. Consider extracting the merge into a small helper shared with (or invoked from) the adapter — or at minimum add a table-test that locks in the current semantics so the drift gets caught in CI.
  • On failure you return NewGenericValidationError while the sibling CreatePlanAddon path returns the same shape, which is consistent — 👍. Just double-check the underlying pa.Validate() error path already produces ProductCatalogValidationError extensions when appropriate; wrapping it again here shouldn't flatten those structured details.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/productcatalog/planaddon/service/planaddon.go` around lines 325 -
370, The validation merges incoming patch semantics locally (using params,
planAddon and building pa then calling pa.Validate()) but that duplicates the
patch-merge rules in planaddon/adapter/planaddon.go::UpdatePlanAddon and risks
drift; refactor the merge into a single helper (e.g., MergePlanAddonPatch or
ApplyPlanAddonPatch) that encapsulates the SetOrClearMaxQuantity / nil-skip
behavior and is called from both UpdatePlanAddon and this validation path (use
symbols planAddon, params, pa.Validate()), and add a table-driven unit test that
asserts the current merge outcomes for combinations of nil/non-nil MaxQuantity,
FromPlanPhase, Metadata and Annotations to lock the semantics in CI. Ensure the
helper preserves existing semantics (MaxQuantity uses params.MaxQuantity as-is,
FromPlanPhase/Metadata/Annotations are only overwritten when non-nil).
e2e/planaddons_v3_test.go (3)

313-331: Good call pinning observed behavior — leave a breadcrumb for the spec.

The comment already notes the doc-vs-server mismatch (404 vs 400 "plan is deleted"). Since this test will now enforce the 400 contract, consider opening a follow-up issue (or TODO with a link) to reconcile the OpenAPI doc with reality — otherwise the next person hitting the docs will assume 404 and be confused by this test.

Happy to draft the issue if you want.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 313 - 331, Test enforces a 400 "plan
is deleted" contract that contradicts the OpenAPI doc's 404; add a short TODO
comment in the test "detach from a deleted plan → 400 'plan is deleted'" (near
setupAttachedPlanAddon / PublishPlan / ArchivePlan / DeletePlan / DetachAddon)
that creates an issue or links an existing issue to reconcile the OpenAPI spec
with the observed server behavior so future readers know this mismatch is
intentional and tracked.

354-372: Minor: assertion order nit.

Line 371's require.NotNil(t, problem) runs after assert.Equal has already read problem in its format arg on line 370. If problem is nil, the formatter handles it fine, but the NotNil check is then redundant for the preceding line and doesn't guard anything. Either move require.NotNil(t, problem) before the status assert, or drop it — same pattern also shows up in TestV3PlanAddonDuplicateAttachmentRejected (line 393).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 354 - 372, The assertion order is
unsafe because assert.Equal in TestV3PlanAddonPublishAgainstArchivedAddon reads
the `problem` variable in its format string before the subsequent
require.NotNil(t, problem) can guard it; move the require.NotNil(t, problem)
call to immediately before the status assertion (i.e., place it right after the
ArchiveAddon/PublishPlan call that sets `problem`) or remove the redundant
NotNil, and apply the same change to TestV3PlanAddonDuplicateAttachmentRejected
so `problem` is checked before any formatting that references it.

152-186: Brittle substring match on server error strings.

expectedDetailIn values like "invalid active status, allowed statuses: [draft scheduled]" couple the e2e suite to the exact phrasing and slice-formatting of domain error messages. A harmless rewording (or switching from fmt.Sprintf("%v", slice) to a comma-joined list) will turn these into red flakes even though behavior is correct. If the API exposes a stable error code or type/extensions field for these, asserting on that would be a lot more durable; otherwise maybe match on just "allowed statuses" + the offending status keyword.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 152 - 186, The test uses brittle
full-string substring checks via expectedDetailIn in the table-driven cases;
update the assertions in the test loop that reads expectedDetailIn to instead
validate a stable error identifier or structured field (e.g., response JSON
"code", "type", or "extensions") when available, and if not available assert on
minimal, robust fragments such as the phrase "allowed statuses" plus the
specific offending status (use the planStatus/addonStatus values from each case)
rather than the full formatted slice text; locate the assertion logic that
consumes expectedDetailIn in the test function and replace it with these more
durable checks referencing expectedDetailIn (or replace expectedDetailIn entries
with the small fragments) so rewording or slice formatting changes won't break
the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/planaddons_v3_test.go`:
- Around line 313-331: Test enforces a 400 "plan is deleted" contract that
contradicts the OpenAPI doc's 404; add a short TODO comment in the test "detach
from a deleted plan → 400 'plan is deleted'" (near setupAttachedPlanAddon /
PublishPlan / ArchivePlan / DeletePlan / DetachAddon) that creates an issue or
links an existing issue to reconcile the OpenAPI spec with the observed server
behavior so future readers know this mismatch is intentional and tracked.
- Around line 354-372: The assertion order is unsafe because assert.Equal in
TestV3PlanAddonPublishAgainstArchivedAddon reads the `problem` variable in its
format string before the subsequent require.NotNil(t, problem) can guard it;
move the require.NotNil(t, problem) call to immediately before the status
assertion (i.e., place it right after the ArchiveAddon/PublishPlan call that
sets `problem`) or remove the redundant NotNil, and apply the same change to
TestV3PlanAddonDuplicateAttachmentRejected so `problem` is checked before any
formatting that references it.
- Around line 152-186: The test uses brittle full-string substring checks via
expectedDetailIn in the table-driven cases; update the assertions in the test
loop that reads expectedDetailIn to instead validate a stable error identifier
or structured field (e.g., response JSON "code", "type", or "extensions") when
available, and if not available assert on minimal, robust fragments such as the
phrase "allowed statuses" plus the specific offending status (use the
planStatus/addonStatus values from each case) rather than the full formatted
slice text; locate the assertion logic that consumes expectedDetailIn in the
test function and replace it with these more durable checks referencing
expectedDetailIn (or replace expectedDetailIn entries with the small fragments)
so rewording or slice formatting changes won't break the tests.

In `@openmeter/productcatalog/planaddon/service/planaddon.go`:
- Line 376: The log message logger.Debug("validating plan add-on assignment") is
stale because it appears after the validation block and just before the adapter
write; update it to accurately reflect the action (e.g.,
logger.Debug("persisting plan add-on assignment")) or move the existing debug
call up next to the actual validation step in the function that performs
validation and adapter write (refer to the validate block and the adapter write
call in planaddon.go around the function handling plan add-on assignment) so
logs match the real operation.
- Around line 325-370: The validation merges incoming patch semantics locally
(using params, planAddon and building pa then calling pa.Validate()) but that
duplicates the patch-merge rules in
planaddon/adapter/planaddon.go::UpdatePlanAddon and risks drift; refactor the
merge into a single helper (e.g., MergePlanAddonPatch or ApplyPlanAddonPatch)
that encapsulates the SetOrClearMaxQuantity / nil-skip behavior and is called
from both UpdatePlanAddon and this validation path (use symbols planAddon,
params, pa.Validate()), and add a table-driven unit test that asserts the
current merge outcomes for combinations of nil/non-nil MaxQuantity,
FromPlanPhase, Metadata and Annotations to lock the semantics in CI. Ensure the
helper preserves existing semantics (MaxQuantity uses params.MaxQuantity as-is,
FromPlanPhase/Metadata/Annotations are only overwritten when non-nil).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 13d45d76-a7dd-42d1-ab16-f7b6926a8564

📥 Commits

Reviewing files that changed from the base of the PR and between 9722196 and 01d4bc0.

📒 Files selected for processing (2)
  • e2e/planaddons_v3_test.go
  • openmeter/productcatalog/planaddon/service/planaddon.go

@rolosp rolosp force-pushed the chore/productcatalog-v3-e2e branch from d99cbe8 to f4a39a7 Compare April 22, 2026 12:41
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
e2e/addons_v3_test.go (2)

248-253: Heads-up: substring assertion is coupled to a server-side error string.

"feature with ID <id> not found" will break silently if anyone tweaks that error message upstream. Since the comment already notes the PC code rate_card_feature_not_found isn't yet surfaced in extensions.validationErrors, consider leaving a TODO to switch to assertValidationCode once it is — that way the intent is visible and the brittle string match has a clear exit plan.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/addons_v3_test.go` around lines 248 - 253, The test currently uses a
brittle substring assertion via assertProblemDetail(t, problem, "feature with ID
"+fakeFeatureID+" not found"); replace this with a TODO and a more robust check:
keep the existing assertProblemDetail for now but add a clear TODO comment
referencing rate_card_feature_not_found and assertValidationCode so future
changes can swap to assertValidationCode when the server surfaces the PC code;
specifically update the test around CreateAddon to (1) retain or slightly relax
the existing assertion to avoid strict dependency on exact wording, (2) add a
TODO mentioning rate_card_feature_not_found and assertValidationCode, and (3)
annotate use of fakeFeatureID and the assertProblemDetail/ assertValidationCode
functions so the intent and migration path are obvious.

285-318: Tiny nit: expectedStatus is effectively constant here.

Both rows expect http.StatusCreated, so the field isn't pulling weight yet. Totally fine to keep as scaffolding for the "re-enable once validator is wired" rows called out in the comment above — just flagging in case you'd rather drop it until those rows land. Also, require.NotNil(t, addon) on line 316 will only be safe while every case is a 2xx; worth revisiting when you add non-201 cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/addons_v3_test.go` around lines 285 - 318, The test
TestV3AddonInstanceTypePriceCompatibility defines an expectedStatus field that
is identical (http.StatusCreated) for all cases and uses require.NotNil(t,
addon) which only holds for 2xx responses; simplify by removing the
expectedStatus field from the cases and assert status == http.StatusCreated
directly (or replace the per-case field with a single constant variable if you
prefer), and update the require.NotNil check to run only when status is a 2xx
(e.g., if status >= 200 && status < 300 { require.NotNil(t, addon) } ) so the
assertion remains safe when non-201 cases are later added; target symbols:
TestV3AddonInstanceTypePriceCompatibility, expectedStatus, require.NotNil.
e2e/planaddons_v3_test.go (1)

97-110: Consider plugging two small coverage gaps tied to this PR's new behavior.

This PR specifically introduces post-merge PlanAddon.Validate() on UpdatePlanAddon, but the lifecycle update sub-test (Lines 97-110) only walks the happy path. A failing-update case (e.g., patching FromPlanPhase to a phase that doesn't exist on the plan, or otherwise driving the post-merge state to fail Validate()) would directly lock in the new validation behavior called out in the PR description.

Additionally, the TestV3PlanAddonInstanceTypeMaxQuantityMatrix table (Lines 213-286) covers multiple + {nil, 5, 0} and single + nil, but misses single + positive max_quantity, which per PlanAddon.Validate() in openmeter/productcatalog/planaddon.go should be rejected with ErrPlanAddonMaxQuantityMustNotBeSet. That's the one branch in the matrix exercised purely by the domain validator (not the schema binder), so it's worth pinning.

Happy to draft both cases if useful — otherwise totally fine to defer. Flagging as recommended since the rest of the matrix is thorough.

As per coding guidelines: "Make sure the tests are comprehensive and cover the changes."

Also applies to: 213-286

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/planaddons_v3_test.go` around lines 97 - 110, Add two failing-path tests
to cover the new post-merge validation: 1) in the lifecycle update sub-test
around UpdatePlanAddon, add a case that sets FromPlanPhase to a non-existent
phase (or otherwise drives post-merge state to fail PlanAddon.Validate()) and
assert the response is a non-200 status and the returned problem/error indicates
validation failed (use UpdatePlanAddon and PlanAddon.Validate as anchors). 2) In
TestV3PlanAddonInstanceTypeMaxQuantityMatrix, add the missing table row for
instance_type == "single" with a positive max_quantity and assert the request is
rejected with the domain validation error ErrPlanAddonMaxQuantityMustNotBeSet
(reference TestV3PlanAddonInstanceTypeMaxQuantityMatrix and
ErrPlanAddonMaxQuantityMustNotBeSet). Ensure assertions check HTTP
status/problem or returned error matches the expected validation failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@e2e/addons_v3_test.go`:
- Around line 248-253: The test currently uses a brittle substring assertion via
assertProblemDetail(t, problem, "feature with ID "+fakeFeatureID+" not found");
replace this with a TODO and a more robust check: keep the existing
assertProblemDetail for now but add a clear TODO comment referencing
rate_card_feature_not_found and assertValidationCode so future changes can swap
to assertValidationCode when the server surfaces the PC code; specifically
update the test around CreateAddon to (1) retain or slightly relax the existing
assertion to avoid strict dependency on exact wording, (2) add a TODO mentioning
rate_card_feature_not_found and assertValidationCode, and (3) annotate use of
fakeFeatureID and the assertProblemDetail/ assertValidationCode functions so the
intent and migration path are obvious.
- Around line 285-318: The test TestV3AddonInstanceTypePriceCompatibility
defines an expectedStatus field that is identical (http.StatusCreated) for all
cases and uses require.NotNil(t, addon) which only holds for 2xx responses;
simplify by removing the expectedStatus field from the cases and assert status
== http.StatusCreated directly (or replace the per-case field with a single
constant variable if you prefer), and update the require.NotNil check to run
only when status is a 2xx (e.g., if status >= 200 && status < 300 {
require.NotNil(t, addon) } ) so the assertion remains safe when non-201 cases
are later added; target symbols: TestV3AddonInstanceTypePriceCompatibility,
expectedStatus, require.NotNil.

In `@e2e/planaddons_v3_test.go`:
- Around line 97-110: Add two failing-path tests to cover the new post-merge
validation: 1) in the lifecycle update sub-test around UpdatePlanAddon, add a
case that sets FromPlanPhase to a non-existent phase (or otherwise drives
post-merge state to fail PlanAddon.Validate()) and assert the response is a
non-200 status and the returned problem/error indicates validation failed (use
UpdatePlanAddon and PlanAddon.Validate as anchors). 2) In
TestV3PlanAddonInstanceTypeMaxQuantityMatrix, add the missing table row for
instance_type == "single" with a positive max_quantity and assert the request is
rejected with the domain validation error ErrPlanAddonMaxQuantityMustNotBeSet
(reference TestV3PlanAddonInstanceTypeMaxQuantityMatrix and
ErrPlanAddonMaxQuantityMustNotBeSet). Ensure assertions check HTTP
status/problem or returned error matches the expected validation failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: baa46d24-b69d-465f-aa3f-047998f9f119

📥 Commits

Reviewing files that changed from the base of the PR and between d99cbe8 and f4a39a7.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (12)
  • .agents/skills/e2e/SKILL.md
  • api/spec/packages/aip/src/productcatalog/planaddon.tsp
  • api/v3/api.gen.go
  • api/v3/handlers/plans/convert.go
  • api/v3/handlers/plans/convert_test.go
  • e2e/addons_v3_test.go
  • e2e/planaddons_v3_test.go
  • e2e/plans_v3_test.go
  • e2e/v3helpers_test.go
  • openmeter/productcatalog/errors.go
  • openmeter/productcatalog/planaddon.go
  • openmeter/productcatalog/planaddon/service/planaddon.go
💤 Files with no reviewable changes (1)
  • api/v3/handlers/plans/convert_test.go
✅ Files skipped from review due to trivial changes (6)
  • openmeter/productcatalog/errors.go
  • openmeter/productcatalog/planaddon.go
  • api/spec/packages/aip/src/productcatalog/planaddon.tsp
  • .agents/skills/e2e/SKILL.md
  • e2e/v3helpers_test.go
  • e2e/plans_v3_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/v3/handlers/plans/convert.go
  • openmeter/productcatalog/planaddon/service/planaddon.go

@rolosp rolosp force-pushed the chore/productcatalog-v3-e2e branch from f4a39a7 to 4641a9c Compare April 22, 2026 13:50
Comment thread api/spec/packages/aip/src/productcatalog/planaddon.tsp Outdated
Comment thread api/v3/handlers/plans/convert.go Outdated
tothandras
tothandras previously approved these changes Apr 22, 2026
@rolosp rolosp merged commit d5e2d1a into main Apr 23, 2026
28 checks passed
@rolosp rolosp deleted the chore/productcatalog-v3-e2e branch April 23, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants