Skip to content

feat(billing): add BulkUpdateBillingGroup RPC for bulk invoice settings update - #892

Open
quantdale wants to merge 2 commits into
mainfrom
feat/bulk-invoice-settings-release
Open

feat(billing): add BulkUpdateBillingGroup RPC for bulk invoice settings update#892
quantdale wants to merge 2 commits into
mainfrom
feat/bulk-invoice-settings-release

Conversation

@quantdale

@quantdale quantdale commented Sep 6, 2026

Copy link
Copy Markdown

Feature

Ripple Bulk Invoice Settings + Update — new RPC Billing.BulkUpdateBillingGroup.

API route

  • POST /v1/billinggroups:bulkUpdate → long-running operation (protos.Operation); poll via GET /ops/v1/{name}

Contract

  • Request: csvContent, delimiter, hasHeader, vendor (aws/azure/gcp), dryRun
  • hasHeader semantics (matches the actual producer, mobingilabs/ouchan ParseCSVForBulkUpdate): true → the first CSV line names the columns and columns are matched by name; false or omitted → the CSV is interpreted in the canonical bulk-update column order (the downloadable template, headerless). Callers SHOULD set it explicitly (the Ripple UI always does). Unlike BulkCreateBillingGroup — which always treats the first CSV line as a header — Bulk Update honors this field as given, and the value is preserved on the async execution payload. (Plain bool, field number 3, mirroring the sibling Bulk Create request; proto3 optional was deliberately NOT used — see "Contract decisions" below.)
  • Per-row result (BulkBillingGroupInvoiceSettingsRow): canonical logical CSV row index (zero-based, header excluded when hasHeader is true, never compressed after malformed/missing rows), user-facing billing-group ID, status (valid/invalid/not_found/updated/failed; created reserved for the Create side), errors, explicit disposition (ready/invalid/not_found/connection_resolution_failed/unsupported_field/updated/failed — documented to exactly match the producer vocabulary), physical csvLineNumber, normalized Invoice Settings with per-field source classification (uploaded/existing/default_derived/org_derived/connection_resolved)
  • BulkUpdateBillingGroupError.reason: not_found / validation_failed / connection_resolution_failed / unsupported_field; index uses the same canonical row-index definition as row result index
  • NormalizedInvoiceSettings: 22 fields (calcType..serviceDiscounts keep numbers 1-8; currency..supportFeeAdjustment added as 9-22)

Compatibility statement

Wire-level compatibility proven by tooling, not line counts:

  • buf breaking --against origin/main (FILE rules, submodule-resolved against-ref) — zero findings
  • Compiled descriptor-set diff (protoc --descriptor_set_out, main vs PR head):
    • RPCs removed: 0; RPC signatures changed: 0; HTTP routes changed/removed: 0
    • Existing fields removed/renumbered/type-changed: 0; messages renamed: 0
    • New: 6 messages, 1 method, 1 additive repeated field
  • BulkCreateBillingGroup impact (exact): existing fields, field numbers (1-9), RPC signature, HTTP route, and semantics are unchanged. BulkCreateBillingGroupMetadata receives ONE new additive repeated field (rows = 9, BulkBillingGroupInvoiceSettingsRow) so Create and Update share one normalized row-result shape (per the OpenSpec design decision D2 and the shared preview contract; the Ripple UI consumes this field typed-optional). Older protobuf clients ignore the unknown field; no existing field or accessor changes. No producer populates it until the Create-side rollout — it is additive and optional on the wire.
  • Requests vendor/csvContent are documented "Required." in comments (mirroring BulkCreateBillingGroupRequest exactly). google.api.field_behavior annotations were deliberately NOT added: the bulk-RPC request family (BulkCreate/Tag/Link) consistently omits them and enforces requiredness server-side; annotating only the new sibling would create swagger asymmetry with Bulk Create.

Contract decisions (reviewer-relevant)

  1. hasHeader kept as plain bool (field 3, same as Bulk Create) with truthful comments instead of optional bool: converting to proto3 optional would change the generated API surface of a brand-new message family mid-review, require presence-threading through the async PubSub payload in the consumer (ouchan), and diverge from the sibling request it intentionally mirrors. The comment now documents omitted=false behavior unambiguously.
  2. BulkCreateBillingGroupMetadata.rows = 9 kept: required by the shared normalized-preview contract (OpenSpec billing-group-bulk-invoice-settings-preview covers the Create-bulk page; the UI consumes metadata.rows for Create dry-runs too). Wire-additive only.
  3. Vocabulary documented to exactly match the producer (mobingilabs/ouchan corrective implementation): operation status pending/processing/completed/partial_failed/failed; row status/disposition and error reason values as listed above; source classifications uploaded/existing/default_derived/org_derived/connection_resolved ("" = unclassified).

SDK regeneration impact

On merge to main, CI (.github/workflows/main.yml) regenerates and pushes blue-sdk-go, blue-sdk-ts, blue-sdk-python, blueapidocs. Locally reproduced from this exact head (db01822) with CI tool versions:

  • protoc 27.0, buf 1.72.0, protoc-gen-go v1.36.5, protoc-gen-go-grpc v1.4.0, grpc-gateway/openapiv2 v2.30.0, submodule protos d76357f6
  • Go: module builds + go test ./... all pass; symbols BulkUpdateBillingGroup, BulkUpdateBillingGroupRequest, BulkUpdateBillingGroupMetadata, BulkBillingGroupInvoiceSettingsRow, NormalizedInvoiceSettings present (incl. gateway route)
  • TS: tsc build clean; npm pack tarball verified to contain bulkUpdateBillingGroup + all 22 normalized fields + all result/metadata types
  • Python: compileall + import/descriptor check clean (two pre-existing broken imports — protos, protoc_gen_openapiv2 — exist identically on current main; not introduced by this PR)
  • No naming collisions (no pre-existing Normalized*/BulkBillingGroup* symbols in any SDK)

Swagger

openapiv2/apidocs.swagger.json regenerated (protoc-gen-openapiv2): purely additive vs origin/main — new route /v1/billinggroups:bulkUpdate (operationId Billing_BulkUpdateBillingGroup) + new request definition; the only changed line vs the previous commit is the corrected hasHeader description. No unrelated churn.

Validation

  • buf build — OK
  • buf breaking --against origin/main (with submodule-resolved against worktree; lint-rule workaround for the pre-existing FIELD_NO_DESCRIPTOR config issue) — zero findings
  • buf lint — fails identically on clean origin/main ("FIELD_NO_DESCRIPTOR" is not a known rule or category ID); pre-existing config/toolchain mismatch, not a CI gate
  • CI Build on this PR: PASS (previous head f35a95c; re-running on db01822)

OpenSpec

alphauslabs/ripple-openspec#82 — add-billing-group-bulk-invoice-settings-update. No OpenSpec change required: the spec intentionally leaves the string vocabulary to the API contract and makes no default-true hasHeader claim; this PR only documents the already-specified behavior accurately.

Safety

No production mutations. No SDK publication from this PR. Deployment/publication happens only through the existing main-branch CI pipeline after human merge.

…gs update

Additive contract change:
- Billing.BulkUpdateBillingGroup, POST /v1/billinggroups:bulkUpdate,
  long-running operation (protos.Operation)
- Request: csvContent, delimiter, hasHeader, vendor, dryRun
- Per-row results (BulkBillingGroupInvoiceSettingsRow): canonical CSV row
  index, billing group ID, status, errors, disposition, csvLineNumber,
  normalized settings with per-field source classification
- NormalizedInvoiceSettings extended additively to 22 fields (fields 1-8
  retain numbers; 9-22 added)
- Blank Update cells preserve existing values; missing billing group fails
  per row; dry-run shares normalization with execution
- BulkCreateBillingGroup shapes unchanged
- openapiv2/apidocs.swagger.json regenerated (additive delta only)

OpenSpec: alphauslabs/ripple-openspec PR #82
(add-billing-group-bulk-invoice-settings-update)
…tual semantics

Reviewer-proofing only — no wire/message/field changes:
- hasHeader: document the ACTUAL semantics (true = first line names columns;
  false OR omitted = canonical bulk-update column order, headerless; callers
  SHOULD set it explicitly; value preserved on the async payload; contrast
  with BulkCreateBillingGroup which always treats the first line as header).
  The previous 'Defaults to true' comment did not match the producer.
- row disposition: document the full producer vocabulary including
  'connection_resolution_failed' and 'unsupported_field'.
- error reason: add 'unsupported_field'; align index comment with the
  canonical row-index definition (zero-based, header excluded, never
  compressed) shared with BulkBillingGroupInvoiceSettingsRow.index.

Verified: buf build OK; descriptor-level buf breaking vs origin/main
clean (FILE rules, zero findings); descriptor-set diff vs main shows only
6 added messages, 1 added method, 1 additive repeated field
(BulkCreateBillingGroupMetadata.rows=9); SDKs regenerated and validated
from this head (Go build+test ./... green, TS tsc+pack green with 22/22
normalized fields, Python compile/import clean — with stubs for two
pre-existing broken imports identical on current main).
@quantdale

Copy link
Copy Markdown
Author

Corrective review pass complete — head updated to db01822 (CI green, re-requesting your review). No wire/message/field changes were made; this pass was reviewer-proofing only:

  1. hasHeader semantics corrected — the previous "Defaults to true" comment did NOT match the actual producer (ouchan's ParseCSVForBulkUpdate treats false/omitted as the canonical headerless column order; Bulk Create ignores the field and always reads a header). The comment now states the real behavior unambiguously. Plain bool kept (mirrors the sibling request; proto3 optional deliberately avoided — see Contract decisions in the description).
  2. Vocabulary documented to exactly match the producer — added connection_resolution_failed + unsupported_field to the row-disposition comment and unsupported_field to the error-reason comment (both are produced by ouchan and rendered by the Ripple UI but were previously undocumented in the API).
  3. BulkUpdateBillingGroupError.index comment aligned with the canonical row-index definition shared with BulkBillingGroupInvoiceSettingsRow.index.
  4. Compatibility now proven by tooling: buf breaking --against origin/main (submodule-resolved) = zero findings; compiled descriptor-set diff = 6 added messages, 1 added method, 1 additive repeated field (BulkCreateBillingGroupMetadata.rows=9), zero changes to existing fields/RPCs/routes.
  5. SDKs regenerated and validated from this exact head (Go build+tests green; TS tsc + npm-pack tarball verified with all 22 normalized fields; Python import/descriptor clean — two pre-existing broken imports exist identically on current main).
  6. Swagger regenerated; only additive vs main.

The description's compatibility section now states precisely: existing BulkCreateBillingGroup fields/numbers/RPC/route/semantics unchanged, with one additive repeated metadata field (rows = 9) that older clients ignore.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants