Skip to content

feat(api): support removing customer billing data via v3 PUTs - #4885

Draft
tothandras wants to merge 1 commit into
mainfrom
fix/api-v3-billingprofiles
Draft

feat(api): support removing customer billing data via v3 PUTs#4885
tothandras wants to merge 1 commit into
mainfrom
fix/api-v3-billingprofiles

Conversation

@tothandras

@tothandras tothandras commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

PUT /customers/{id}/billing and /billing/app-data now replace the stored state: a provided value replaces it, while omitting an optional field (or setting it to null, which is equivalent) removes it — the billing profile override is unpinned and the resolved app's customer data is deleted. Invalid stripe data returns 400 on every profile type.

The nullable request shape comes from the new Shared.UpsertNullableRequest<T> template backed by the @withNullableOptionalProperties decorator, so SDK clients can state removal explicitly with null:

await openmeter.customers.billing.update(customerId, {
  appData: { stripe: null }, // deletes the customer's stripe data
})

Notes:

  • Follows v1's lead on deletions being explicit domain operations (app.DeleteCustomerData, billing.DeleteCustomerOverride), both idempotent — removing absent data is a 200 no-op.
  • The previous "stripe data is required" guard is gone: pinning a stripe-backed profile without stripe data is an honest replace; missing data surfaces at invoice time (v1's DELETE endpoint had no such guard either).
  • Merge/partial updates are intentionally out of scope; they belong on a future PATCH operation (precedent: feature unit_cost).

🤖 Generated with Claude Code

Greptile Summary

The PR changes the v3 customer billing PUT endpoints to replacement semantics, allowing omitted or explicitly null values to remove profile overrides and app customer data.

  • Adds nullable upsert request shapes across TypeSpec, OpenAPI, and generated SDK models.
  • Centralizes app-data validation, replacement, deletion, and response conversion.
  • Adds handler, SDK wire-format, and end-to-end coverage for replacement and removal behavior.

Confidence Score: 4/5

The PR is not yet safe to merge because a failed billing-profile override mutation can still leave the earlier app-data replacement committed.

The full billing PUT performs app customer-data mutation before independently upserting or deleting the profile override, so a later override failure returns an error after persisting only part of the requested replacement.

Files Needing Attention: api/v3/handlers/customers/billing/update_billing.go

Important Files Changed

Filename Overview
api/v3/handlers/customers/billing/update_billing.go Implements full billing-state replacement, but the previously reported non-atomic sequencing of app-data and override mutations remains.
api/v3/handlers/customers/billing/app_data.go Centralizes app-specific validation, deletion, upsert, and response conversion.
api/v3/handlers/customers/billing/update_billing_app_data.go Adopts the shared replacement helper for the app-data-only PUT endpoint.
api/spec/packages/aip/src/customers/billing.tsp Defines nullable replacement request contracts for billing profile and app customer data.
api/spec/packages/aip/lib/decorators.js Adds the decorator support used to make optional upsert properties explicitly nullable.
e2e/customer_billing_appdata_v3_test.go Covers app-data replacement, explicit null removal, omission removal, and idempotent deletion behavior.

Sequence Diagram

sequenceDiagram
  participant Client
  participant API as Customer Billing PUT
  participant App as App Customer Data
  participant Billing as Billing Profile Override
  Client->>API: PUT replacement request
  API->>App: Upsert or delete customer data
  App-->>API: Result
  API->>Billing: Pin or remove override
  Billing-->>API: Result
  API-->>Client: Resolved billing data
Loading

Reviews (3): Last reviewed commit: "feat(api): support removing customer bil..." | Re-trigger Greptile

Context used (3)

Summary by CodeRabbit

  • New Features
    • Customer billing updates now support replacing, clearing, or leaving app data and billing profiles unchanged.
    • Billing profiles can be reset to the default by omitting or explicitly clearing the profile.
    • Stripe and external invoicing customer data can be added, replaced, or removed.
  • Bug Fixes
    • Improved validation and handling of unsupported or incomplete app data.
  • Tests
    • Added coverage for nullable updates, data replacement, clearing behavior, validation, and response mapping.

@tothandras
tothandras requested a review from a team as a code owner August 8, 2026 11:46
@tothandras tothandras added release-note/bug-fix Release note: Bug Fixes release-note/feature Release note: Exciting New Features and removed release-note/bug-fix Release note: Bug Fixes labels Aug 8, 2026
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Customer billing and app-data upserts now support omitted, null, and replacement values. TypeSpec contracts, generated models, SDK mappings, billing handlers, and unit and end-to-end tests were updated.

Changes

Customer billing tri-state updates

Layer / File(s) Summary
Nullable request contracts
api/spec/AGENTS.md, api/spec/packages/aip/..., api/spec/packages/aip-client-javascript/src/models/*
Added Shared.UpsertNullableRequest<T>. Updated billing and app-data request models and SDK schemas for omitted, null, and replacement values.
Generated nullable models
api/v3/api.gen.go, api/v3/client/models_customers.go
Changed app-data and billing-profile fields to use Nullable representations. Regenerated the embedded OpenAPI specification.
Billing handler flow
api/v3/handlers/customers/billing/*
Added shared app-data retrieval and mutation helpers. Billing updates now resolve omitted, null, and specified profiles, apply app data, and rebuild responses.
Handler and end-to-end validation
api/spec/packages/aip-client-javascript/tests/wire.spec.ts, api/v3/handlers/customers/billing/app_data_test.go, e2e/*billing*
Added coverage for wire null preservation, app-specific data mutations, profile fallback, validation errors, deletion, omission, replacement, and empty updates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant UpdateCustomerBilling
  participant applyAppData
  participant CustomerDataStore
  participant buildAppData
  Client->>UpdateCustomerBilling: Send nullable billing profile and app data
  UpdateCustomerBilling->>applyAppData: Apply omitted, null, or replacement app data
  applyAppData->>CustomerDataStore: Delete or upsert customer data
  UpdateCustomerBilling->>buildAppData: Build response app data
  buildAppData-->>Client: Return resolved billing profile and app data
Loading

Possibly related PRs

Suggested labels: area/billing, kind/feature

Suggested reviewers: chrisgacsal, turip

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding removal support for customer billing data through v3 PUT endpoints.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/api-v3-billingprofiles

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.

Comment thread api/v3/handlers/customers/billing/update_billing.go Outdated
Comment thread api/v3/handlers/customers/billing/app_data.go Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

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

33-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: add given/when/then intent comments to the setup subtest.

This multi-step subtest installs an app, creates a profile and a customer, then pins the profile. The sibling e2e test in this PR (e2e/billinginvoice_override_test.go) opens each lifecycle subtest with given, when, and then comments. Matching that here would make the fixture chain easier to follow. The rest of the file reads well, and the tri-state coverage is thorough.

As per coding guidelines: "Begin non-trivial service or lifecycle subtests with concise given, when, and then intent comments."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/customer_billing_appdata_v3_test.go` around lines 33 - 79, The setup
subtest currently lacks lifecycle intent comments. Add concise given, when, and
then comments around the setup flow in the runRequired callback, covering the
external invoicing app/profile/customer preparation and the billing-profile
assignment, matching the style used by the sibling lifecycle test.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/spec/packages/aip/src/customers/billing.tsp`:
- Around line 28-50: The optional-field documentation in
CustomerBillingDataUpsertRequest incorrectly implies that every field accepts
null. Update the comments in
api/spec/packages/aip/src/customers/billing.tsp:28-50 to state that
null-to-unset behavior applies only to billing_profile and nested app-data
fields that are nullable; do not document app_data itself as nullable.
Regenerate the corresponding documentation in
api/spec/packages/aip-client-javascript/src/models/types.ts:3422-3437 from the
corrected TypeSpec source, with no direct schema change required there.

In `@api/spec/packages/aip/src/shared/request.tsp`:
- Around line 31-37: The UpsertNullableRequest template currently emits the same
friendly name as UpsertRequest, creating component-name collisions. Update the
friendlyName declaration on UpsertNullableRequest to use a distinct
nullable-specific generated name, while preserving its visibility and
nullable-property decorators.

In `@api/v3/handlers/customers/billing/app_data.go`:
- Around line 70-90: Update validateAppData to accept a field-prefix parameter
and use it when constructing the invalid parameter field for Stripe Customer ID,
preserving the standalone app-data path. Update applyAppData and each calling
handler to pass the appropriate prefix, including app_data.stripe for the
billing endpoint so validation errors match the request body paths.

In `@api/v3/handlers/customers/billing/update_billing.go`:
- Around line 140-158: Update the Stripe pinning validation in the billing
update handler so pinning is rejected when appData.Stripe.IsNull(), returning
the same 400 invalid-parameter response used for missing Stripe data before
applyAppData or UpsertCustomerOverride runs. Preserve the existing customer-data
lookup for unspecified Stripe data, and add a handler or end-to-end regression
covering deletion followed by pinning null.

---

Nitpick comments:
In `@e2e/customer_billing_appdata_v3_test.go`:
- Around line 33-79: The setup subtest currently lacks lifecycle intent
comments. Add concise given, when, and then comments around the setup flow in
the runRequired callback, covering the external invoicing app/profile/customer
preparation and the billing-profile assignment, matching the style used by the
sibling lifecycle test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f6c2887-fdd9-45d9-a346-e94a5d59e1a8

📥 Commits

Reviewing files that changed from the base of the PR and between a9a7283 and 18e9b86.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (18)
  • api/spec/AGENTS.md
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip-client-javascript/tests/wire.spec.ts
  • api/spec/packages/aip/lib/decorators.js
  • api/spec/packages/aip/src/customers/billing.tsp
  • api/spec/packages/aip/src/customers/operations.tsp
  • api/spec/packages/aip/src/shared/request.tsp
  • api/v3/api.gen.go
  • api/v3/client/models_customers.go
  • api/v3/handlers/customers/billing/app_data.go
  • api/v3/handlers/customers/billing/app_data_test.go
  • api/v3/handlers/customers/billing/get_billing.go
  • api/v3/handlers/customers/billing/update_billing.go
  • api/v3/handlers/customers/billing/update_billing_app_data.go
  • e2e/billinginvoice_override_test.go
  • e2e/billinginvoices_v3_test.go
  • e2e/customer_billing_appdata_v3_test.go

Comment thread api/spec/packages/aip/src/customers/billing.tsp
Comment on lines +31 to +37
@friendlyName("Upsert{name}Request", T)
@withVisibility(Lifecycle.Create, Lifecycle.Update)
@withNullableOptionalProperties
model UpsertNullableRequest<T extends {}> is DefaultKeyVisibility<
T,
Lifecycle.Read
>;

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find every instantiation of both templates and check for a shared T.
rg -n --glob '*.tsp' 'Shared\.Upsert(Nullable)?Request<' .

Repository: openmeterio/openmeter

Length of output: 1042


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files containing UpsertRequest definitions/usage:"
rg -n --glob '*.tsp' 'UpsertNullableRequest|UpsertRequest|FriendlyName|model Upsert' api/spec/packages/aip/src

echo
echo "request.tsp around definitions:"
cat -n api/spec/packages/aip/src/shared/request.tsp | sed -n '1,80p'

echo
echo "customer files around usages:"
cat -n api/spec/packages/aip/src/customers/operations.tsp | sed -n '70,150p'
cat -n api/spec/packages/aip/src/customers/billing.tsp | sed -n '1,90p'

Repository: openmeterio/openmeter

Length of output: 8568


Heads up: keep the nullable upsert template name distinct.

UpsertRequest and UpsertNullableRequest both emit Upsert{name}Request, but only Apps.AppCustomerData uses the nullable variant. If any model is passed to both templates later, generate a shared component name, so give this template a distinct friendly name or ensure duplicate generation fails loudly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/aip/src/shared/request.tsp` around lines 31 - 37, The
UpsertNullableRequest template currently emits the same friendly name as
UpsertRequest, creating component-name collisions. Update the friendlyName
declaration on UpsertNullableRequest to use a distinct nullable-specific
generated name, while preserving its visibility and nullable-property
decorators.

Source: Path instructions

Comment thread api/v3/handlers/customers/billing/app_data.go Outdated
Comment on lines 140 to 158
pinningProfile := request.BillingProfile.IsSpecified() && !request.BillingProfile.IsNull()
if pinningProfile && application.GetType() == app.AppTypeStripe && !appData.Stripe.IsSpecified() {
if _, err := application.GetCustomerData(ctx, app.GetAppInstanceCustomerDataInput{
CustomerID: request.CustomerID,
}); err != nil {
if app.IsAppCustomerPreConditionError(err) {
return resp, apierrors.NewBadRequestError(ctx, fmt.Errorf("stripe data is required"), apierrors.InvalidParameters{
apierrors.InvalidParameter{
Field: "app_data.stripe",
Rule: "required",
Reason: "Stripe data is required",
Source: apierrors.InvalidParamSourceBody,
},
})
}

if request.AppData.ExternalInvoicing.Labels != nil {
appData = appcustominvoicing.CustomerData{
Metadata: models.Metadata(*request.AppData.ExternalInvoicing.Labels),
}
}
return resp, err
}
case app.AppTypeSandbox:
appData = appsandbox.CustomerData{}
default:
return resp, apierrors.NewInternalError(ctx, fmt.Errorf("unsupported app type: %s", application.GetType()))
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Verify nullable state semantics for the declared dependency version.
curl -fsSL https://raw.githubusercontent.com/oapi-codegen/nullable/v1.2.0/nullable.go |
  rg -n -C 3 'func \(.*Nullable.*\) Is(Specified|Null)'

Repository: openmeterio/openmeter

Length of output: 483


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Locate and inspect the relevant handler and data application code.
fd -i 'update_billing\.go$' .
file="$(fd -i 'update_billing\.go$' . | head -n 1)"
printf '\n--- %s lines 1-240 ---\n' "$file"
sed -n '1,240p' "$file" | nl -ba

printf '\n--- search applyAppData and stripe handling ---\n'
rg -n "applyAppData|AppData|BillingProfile|IsAppCustomerPreConditionError|stripe" -S . --glob '!vendor/**' --glob '!node_modules/**' | head -n 200

Repository: openmeterio/openmeter

Length of output: 328


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# Locate and inspect the relevant handler and data application code.
file="$(fd -i 'update_billing\.go$' . | head -n 1)"
printf '%s\n' "$file"
awk '{ printf "%5d: %s\n", NR, $0 }' "$file"

printf '\n--- search applyAppData and stripe handling ---\n'
rg -n "applyAppData|AppData|BillingProfile|IsAppCustomerPreConditionError|stripe" -S . --glob '!vendor/**' --glob '!node_modules/**' | head -n 250

Repository: openmeterio/openmeter

Length of output: 31418


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' "$file"
awk '{ printf "%5d: %s\n", NR, $0 }' "$file"

printf '\n--- applyAppData and app data API/model ---\n'
rg -n "func applyAppData|type UpsertAppCustomerDataRequest|type AppCustomer|IsSpecified|IsNull|GetCustomerData|AppTypeStripe" -S api openmeter --glob '!vendor/**' --glob '!node_modules/**' | head -n 240

Repository: openmeterio/openmeter

Length of output: 196


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

file="$(fd -i 'update_billing\.go$' . | head -n 1)"
printf '%s\n' "$file"
awk '{ printf "%5d: %s\n", NR, $0 }' "$file"

printf '\n--- applyAppData and app data API/model ---\n'
rg -n "func applyAppData|type UpsertAppCustomerDataRequest|type AppCustomer|AppCustomerDataRequest|stripe Null|GetCustomerData|AppTypeStripe|UpdateAppData|UpsertApp" -S api openmeter --glob '!vendor/**' --glob '!node_modules/**' | head -n 320

Repository: openmeterio/openmeter

Length of output: 29275


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

api_file="$(fd -i '^api\.gen\.go$' . | awk '/\/api\/v3\/api\.gen\.go$/ { print; exit }')"
printf '%s\n' "$api_file"
sed -n '7640,7725p' "$api_file"

printf '\n--- applicable app_data handler implementation ---\n'
awk '{ printf "%5d: %s\n", NR, $0 }' api/v3/handlers/customers/billing/app_data.go

printf '\n--- relevant generated model declaration regions ---\n'
rg -n "func \\(t \\*UpsertAppCustomerDataRequest\".*Nullable|func .*UpsertAppCustomerDataRequest.*IsNull|func .*UpsertAppCustomerDataRequest.*IsSpecified|type UpsertAppCustomerDataRequest \\*" api/v3/api.gen.go openmeter -S

Repository: openmeterio/openmeter

Length of output: 10166


Reject app_data.stripe: null when pinning a Stripe profile.

profile.stripe: null deletes Stripe customer data, and pinning it back does not recheck for that data. If pinningProfile && appType == stripe, handle appData.Stripe.IsNull() as a 400 instead of proceeding to applyAppData and UpsertCustomerOverride. Add a handler or e2e regression for this sequence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v3/handlers/customers/billing/update_billing.go` around lines 140 - 158,
Update the Stripe pinning validation in the billing update handler so pinning is
rejected when appData.Stripe.IsNull(), returning the same 400 invalid-parameter
response used for missing Stripe data before applyAppData or
UpsertCustomerOverride runs. Preserve the existing customer-data lookup for
unspecified Stripe data, and add a handler or end-to-end regression covering
deletion followed by pinning null.

@tothandras
tothandras force-pushed the fix/api-v3-billingprofiles branch from 18e9b86 to 701c332 Compare August 8, 2026 12:02
@tothandras tothandras changed the title feat(api): support null to unset fields on v3 customer billing PUTs feat(api): support removing customer billing data via v3 PUTs Aug 8, 2026
PUT /customers/{id}/billing and /billing/app-data now replace the
stored state: a provided value replaces it, while omitting an optional
field (or setting it to null, which is equivalent) removes it — the
billing profile override is unpinned and the resolved app's customer
data is deleted. Invalid stripe data returns 400 on every profile
type. The nullable request shape comes from the new
Shared.UpsertNullableRequest<T> template backed by the
@withNullableOptionalProperties decorator, so SDK clients can state
removal explicitly with null.
@tothandras
tothandras force-pushed the fix/api-v3-billingprofiles branch from 701c332 to a5bdbbe Compare August 8, 2026 12:10

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/v3/handlers/customers/billing/app_data.go`:
- Around line 71-100: Introduce an ApplyAppDataInput struct containing the
related parameters currently passed to applyAppData, implement its Validate()
error method using the existing validation logic, and change applyAppData to
accept this input. Validate the input before the mutation switch, aggregate
validation failures with errors.Join, and return
models.NewNillableGenericValidationError(...); update all callers to construct
the named input.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a1d40f7c-1a29-4b65-b2fc-7853c7b69da6

📥 Commits

Reviewing files that changed from the base of the PR and between 18e9b86 and a5bdbbe.

⛔ Files ignored due to path filters (1)
  • api/v3/openapi.yaml is excluded by !**/openapi.yaml
📒 Files selected for processing (12)
  • api/spec/AGENTS.md
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip/src/customers/billing.tsp
  • api/spec/packages/aip/src/shared/request.tsp
  • api/v3/api.gen.go
  • api/v3/client/models_customers.go
  • api/v3/handlers/customers/billing/app_data.go
  • api/v3/handlers/customers/billing/app_data_test.go
  • api/v3/handlers/customers/billing/update_billing.go
  • api/v3/handlers/customers/billing/update_billing_app_data.go
  • e2e/customer_billing_appdata_v3_test.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • api/v3/handlers/customers/billing/update_billing_app_data.go
  • api/spec/packages/aip/src/shared/request.tsp
  • api/spec/AGENTS.md
  • e2e/customer_billing_appdata_v3_test.go
  • api/v3/handlers/customers/billing/update_billing.go
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/v3/handlers/customers/billing/app_data_test.go
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/v3/api.gen.go

Comment on lines +71 to +100
func validateAppData(ctx context.Context, fieldPrefix string, data api.UpsertAppCustomerDataRequest) error {
if data.Stripe.IsSpecified() && !data.Stripe.IsNull() {
stripeData, err := data.Stripe.Get()
if err != nil {
return err
}

if stripeData.CustomerId == nil {
return apierrors.NewBadRequestError(ctx, fmt.Errorf("stripe customer id is required"), apierrors.InvalidParameters{
apierrors.InvalidParameter{
Field: fieldPrefix + "stripe.customer_id",
Rule: "required",
Reason: "Stripe Customer ID is required",
Source: apierrors.InvalidParamSourceBody,
},
})
}
}

return nil
}

// applyAppData replaces the customer's data for the resolved payment app: a
// provided value is validated and stored, while an omitted or explicitly null
// field deletes the existing data. Valid fields for apps other than the
// resolved one are ignored.
func applyAppData(ctx context.Context, application app.App, customerID customer.CustomerID, fieldPrefix string, data api.UpsertAppCustomerDataRequest) error {
if err := validateAppData(ctx, fieldPrefix, data); err != nil {
return err
}

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use a named input for applyAppData.

applyAppData accepts five related parameters and performs validation plus persistent mutation. Define ApplyAppDataInput, implement models.Validator, and validate it before the mutation switch. Collect validation errors with errors.Join and return models.NewNillableGenericValidationError(...).

As per coding guidelines, “For non-trivial operations with multiple related parameters, define a named <Operation>Input struct implementing models.Validator through Validate() error.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v3/handlers/customers/billing/app_data.go` around lines 71 - 100,
Introduce an ApplyAppDataInput struct containing the related parameters
currently passed to applyAppData, implement its Validate() error method using
the existing validation logic, and change applyAppData to accept this input.
Validate the input before the mutation switch, aggregate validation failures
with errors.Join, and return models.NewNillableGenericValidationError(...);
update all callers to construct the named input.

Source: Coding guidelines

@tothandras
tothandras marked this pull request as draft August 8, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant