Skip to content

fix(api): harden API — strip PII/Stripe IDs, unify pagination, normalise cents naming#51

Merged
mlehotskylf merged 5 commits into
mainfrom
fix/api-hardening-pii-pagination-naming
May 26, 2026
Merged

fix(api): harden API — strip PII/Stripe IDs, unify pagination, normalise cents naming#51
mlehotskylf merged 5 commits into
mainfrom
fix/api-hardening-pii-pagination-naming

Conversation

@mlehotskylf

@mlehotskylf mlehotskylf commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

API hardening pass before the first external consumer is onboarded: strips internal/Stripe IDs from public responses, introduces a DonationSummary projection for the public donation list, standardises pagination error handling, normalises money field naming to _cents, and unifies transaction pagination to limit/offset.

All four changes are applied together because they touch the same wire format — shipping them as one PR avoids a window where the API is partially cleaned up.


Changes

1 — Strip internal IDs from public responses

Donation and Subscription were serialising user_id, organization_id, and all Stripe IDs (stripe_payment_intent_id, stripe_charge_id, stripe_subscription_id, etc.) to any API caller. These are internal operational fields used only by the webhook reconciliation flow.

  • All internal fields suppressed with json:"-"
  • Added DonationSummary — a purpose-built read model for the public list endpoint (GET /v1/initiatives/{id}/donations) that includes only display-safe fields: donor name, type, avatar, amount, status, and date
  • The full Donation struct is still returned by GET /v1/me/donations (authenticated — caller owns the record)
  • DonationService.ListByInitiative now returns []DonationSummary; a helper batch-resolves donor display info from the DB
  • Frontend BFF: removed stripe_payment_intent_id from wire types; updated donations.post.ts

2 — Return 400 for non-integer pagination params

All list handlers were silently swallowing strconv.Atoi parse errors — ?limit=abc was treated as ?limit=0 with no feedback to the caller.

  • Extracted parsePaginationParams in respond.go: returns 400 Bad Request with an error message when limit or offset is not a valid integer
  • All six list handlers (List/ListForUser across initiatives, donations, subscriptions) now use it
  • Covered by unit tests in respond_test.go

3 — Normalise money fields to _cents

Some fields used _in_cents in their JSON tags, others already used _cents. Standardised everything to _cents:

Field Before After
Donation.CurrentAmountCents current_amount_in_cents amount_cents
DonationCreateInput.AmountCents amount_in_cents amount_cents
Subscription.CurrentAmountCents current_amount_in_cents amount_cents
SubscriptionCreateInput.AmountCents amount_in_cents amount_cents
GoalInput.AmountInCents amount_in_cents amount_cents
OSTIFDetailInput/OSTIFDetail.TotalBudgetInCents total_budget_in_cents total_budget_cents

Breaking wire format change — safe to ship now because no external consumer exists yet; the frontend BFF is updated atomically in this PR.

4 — Unify transaction pagination to limit/offset

TransactionList was using page/size (1-based) while every other paginated resource uses limit/offset. Unified end-to-end:

  • TransactionList model: page/sizelimit/offset
  • Ledger client (TransactionFilter): Page/SizeLimit/Offset; converts to Ledger's 1-based page/perPage internally — upstream contract unchanged
  • InitiativeService.GetTransactions and InitiativeHandler.GetTransactions updated
  • Frontend: BFF server route, transactions.types.ts (server + shared), useInitiativeTransactions composable

Test plan

  • go build ./... and go vet ./... pass
  • pnpm tsc-check passes in frontend/
  • GET /v1/initiatives/{id}/donations no longer includes user_id, organization_id, or any stripe_* field
  • GET /v1/me/donations still returns full donation records for the authenticated user
  • GET /v1/initiatives/{id}/donations?limit=abc returns 400 with an error message
  • Donation and subscription amount fields serialise as amount_cents
  • GET /v1/initiatives/{id}/transactions?limit=5&offset=0 returns correct limit/offset in the response envelope

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 26, 2026 02:37
@mlehotskylf
mlehotskylf force-pushed the fix/api-hardening-pii-pagination-naming branch from 55a6b1d to 5273130 Compare May 26, 2026 02:42

Copilot AI 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.

Pull request overview

This PR hardens the public API wire format by removing internal/operational identifiers from serialized responses, standardizing pagination and money field naming, and aligning frontend BFF/types with the updated backend contracts. It also adds a missing Helm ingress template so existing ingress values are actually applied at deploy time.

Changes:

  • Remove Stripe/internal IDs from donation/subscription JSON output and introduce a public DonationSummary projection for initiative donation listings.
  • Standardize pagination (limit/offset) and enforce integer parsing for pagination query params; unify transaction pagination away from page/size.
  • Normalize JSON field naming from *_in_cents to *_cents across backend models and frontend request/response types; add Helm ingress template.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
frontend/shared/types/transaction.types.ts Align shared transaction list shape to limit/offset.
frontend/shared/types/payment.types.ts Remove Stripe payment intent ID from donation result type.
frontend/server/types/transactions.types.ts Update backend transaction list typing to limit/offset.
frontend/server/types/payment.types.ts Remove stripe_payment_intent_id from backend wire type.
frontend/server/api/static-pages/featured-initiatives.get.ts Fetch featured initiatives using backend-supported sorting by total raised.
frontend/server/api/initiatives/[id]/transactions.get.ts Proxy transactions using limit/offset query params and map updated envelope fields.
frontend/server/api/initiatives/[id]/donations.post.ts Send amount_cents and stop mapping Stripe payment intent ID from the response.
frontend/app/composables/initiatives/useInitiativeTransactions.ts Update composable to request/cache by limit/offset instead of size.
backend/internal/service/initiative_service.go Update service signature to pass limit/offset into the ledger client.
backend/internal/service/donation_service.go Return []DonationSummary for public initiative donation listing and enrich donor display data.
backend/internal/infrastructure/clients/ledger.go Change ledger transaction filter to limit/offset and convert to ledger paging model.
backend/internal/handler/subscription_handler.go Use shared pagination parsing helper and return 400 on invalid pagination params.
backend/internal/handler/respond.go Add parsePaginationParams helper to validate limit/offset.
backend/internal/handler/initiative_handler.go Use new pagination parsing and update transactions handler to accept limit/offset.
backend/internal/handler/donation_handler.go Use new pagination parsing and return public donation summaries for initiative donation list.
backend/internal/domain/models/transaction.go Rename transaction list envelope fields to limit/offset.
backend/internal/domain/models/subscription.go Suppress internal fields from JSON and rename cents fields/tags to *_cents.
backend/internal/domain/models/initiative_relations.go Normalize goal/OSTIF budget JSON field names to *_cents.
backend/internal/domain/models/donation.go Suppress internal fields from JSON, normalize cents tag, and add DonationSummary.
backend/charts/lfx-v2-initiatives-service/templates/ingress.yaml Add ingress template so chart ingress values are no longer a no-op.
Comments suppressed due to low confidence (1)

backend/internal/domain/models/subscription.go:45

  • Now that the JSON field is amount_cents, user-facing validation/errors should refer to amount_cents as well. There are still code paths emitting messages like amount_in_cents must be positive during subscription creation, which will confuse API callers after this wire-format rename.
// SubscriptionCreateInput is the request body for creating a subscription.
type SubscriptionCreateInput struct {
	AmountCents           int64  `json:"amount_cents"`
	Frequency             string `json:"frequency"`
	Category              string `json:"category,omitempty"`
	OrganizationID        string `json:"organization_id,omitempty"`
	StripePaymentMethodID string `json:"stripe_payment_method_id"`

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/internal/infrastructure/clients/ledger.go
Comment thread backend/internal/service/donation_service.go
Comment thread backend/internal/service/donation_service.go
Comment thread backend/internal/domain/models/donation.go
…ise cents naming

Four related API quality issues addressed in one pass so the public wire
format is consistent before the first external consumer is onboarded.

**1 — Strip internal IDs and PII from serialised responses**
`Donation` and `Subscription` were serialising `user_id`, `organization_id`,
and all Stripe internal IDs (`stripe_payment_intent_id`, `stripe_charge_id`,
`stripe_subscription_id`, etc.) directly to API consumers. These are
operational fields used only by the webhook reconciliation flow and must
never appear on public or user-facing endpoints.

Changed all affected fields to `json:"-"`. Added `DonationSummary` — a
purpose-built projection for the public donation list that carries only
display-safe fields (donor name/type/avatar, amount, status, date). The
full `Donation` struct is still used for the authenticated `/v1/me/donations`
endpoint, where the caller owns the record.

Matching frontend BFF types cleaned up: `stripe_payment_intent_id` removed
from `DonationResultWire` and `DonationResult`; the mapping in
`donations.post.ts` simplified accordingly.

**2 — Return 400 for non-integer pagination params**
All list handlers were calling `strconv.Atoi(q.Get("limit"), _)` and
silently ignoring parse errors, causing `?limit=abc` to be treated as
`?limit=0` and fall through to the service default. Callers got no
feedback that their request was malformed.

Extracted a `parsePaginationParams` helper in `respond.go` that returns
a proper `400 Bad Request` with an explanatory message when `limit` or
`offset` cannot be parsed. All six list handlers (`List`/`ListForUser` on
initiatives, donations, and subscriptions) now use it.

**3 — Normalise `_in_cents` suffix to `_cents` across wire format**
Several fields used the redundant `_in_cents` suffix in their JSON tags
(`amount_in_cents`, `current_amount_in_cents`, `total_budget_in_cents`)
while others (transactions, `DonationSummary`) already used the shorter
`_cents`. Standardised everything to `_cents`:
- `Donation.CurrentAmountCents` → `"amount_cents"`
- `DonationCreateInput.AmountCents` → `"amount_cents"`
- `Subscription.CurrentAmountCents` → `"amount_cents"`
- `SubscriptionCreateInput.AmountCents` → `"amount_cents"`
- `GoalInput.AmountInCents` → `"amount_cents"`
- `OSTIFDetailInput/OSTIFDetail.TotalBudgetInCents` → `"total_budget_cents"`

BFF `donations.post.ts` updated to send `amount_cents` in the request body.

**4 — Unify transaction pagination to limit/offset**
`TransactionList` was using `page`/`size` (1-based) while every other
paginated resource used `limit`/`offset`. Unified to `limit`/`offset`
end-to-end: Go model, ledger client (converts internally to Ledger's
1-based page API), service method signature, handler, BFF server route,
frontend server/shared types, and the `useInitiativeTransactions` composable.

**5 — Add missing Helm ingress template**
The backend Helm chart had `ingress` values wired up in ArgoCD but no
`ingress.yaml` template, so `ingress.enabled: true` was silently a no-op.
Added the template following the standard pattern used by other LFX services.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
@mlehotskylf
mlehotskylf force-pushed the fix/api-hardening-pii-pagination-naming branch from 5273130 to f0ac32a Compare May 26, 2026 02:45
…g tests

Three issues identified in PR self-review:

- donation_service: use existing donorTypeOrganization/donorTypeIndividual
  constants from statistics_service instead of inline string literals
- donation_service: record span errors in projectDonationSummaries when
  user or org batch lookups fail (errors are still silenced for graceful
  degradation, but now visible in traces)
- add TestProjectDonationSummaries_* covering: empty input, individual
  donor, org donor, user ID deduplication, user lookup error degradation,
  org lookup error degradation, unknown user ID with no name
- add TestParsePaginationParams_* covering: valid limit+offset, absent
  params defaulting to zero, non-integer limit returns 400, non-integer
  offset returns 400, float rejected, negative integers accepted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings May 26, 2026 02:53

Copilot AI 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.

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.

Comments suppressed due to low confidence (2)

backend/internal/domain/models/donation.go:46

  • DonationCreateInput now uses the JSON field amount_cents, but validation errors in the service layer still refer to amount_in_cents (e.g. “amount_in_cents must be positive”), which will confuse API consumers. Update error messages to match the new wire field name.
// DonationCreateInput is the request body for creating a donation.
type DonationCreateInput struct {
	AmountCents    int64  `json:"amount_cents"`
	Category       string `json:"category,omitempty"`
	PONumber       string `json:"po_number,omitempty"`
	PaymentMethod  string `json:"payment_method,omitempty"`
	OrganizationID string `json:"organization_id,omitempty"`
	// StripePaymentMethodID is used to create a Stripe charge
	StripePaymentMethodID string `json:"stripe_payment_method_id,omitempty"`

backend/internal/domain/models/subscription.go:46

  • SubscriptionCreateInput now uses the JSON field amount_cents, but validation errors in the service layer still refer to amount_in_cents (e.g. “amount_in_cents must be positive”), which will confuse API consumers. Update error messages to match the new wire field name.
// SubscriptionCreateInput is the request body for creating a subscription.
type SubscriptionCreateInput struct {
	AmountCents           int64  `json:"amount_cents"`
	Frequency             string `json:"frequency"`
	Category              string `json:"category,omitempty"`
	OrganizationID        string `json:"organization_id,omitempty"`
	StripePaymentMethodID string `json:"stripe_payment_method_id"`
	// IdempotencyKey is set by the handler from the Idempotency-Key HTTP header.

Comment thread backend/internal/infrastructure/clients/ledger.go
Comment thread backend/internal/handler/initiative_handler.go
Comment thread backend/internal/service/donation_service.go Outdated
Comment thread backend/internal/domain/models/donation.go
Comment thread backend/internal/domain/models/donation.go Outdated
Comment thread backend/internal/handler/respond.go
Address review feedback on PR #51:

- models/donation.go: rename DonorAvatar → DonorAvatarURL (consistent with
  statistics.go), fix doc comment to describe actual omission policy
- service/donation_service.go: propagate span ctx through projectDonationSummaries
  so downstream DB calls are correctly parented; add slog.WarnContext on degraded
  paths; fix error message amount_in_cents → amount_cents
- service/subscription_service.go: fix error message amount_in_cents → amount_cents
- service/donation_service_test.go: update DonorAvatar → DonorAvatarURL references
- clients/ledger.go: document page-alignment constraint on offset/limit conversion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
@mlehotskylf

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commits: 680ee52, 7d32243

Changes Made

  • donation_service.go: Added slog.WarnContext on GetUsersByIDs/GetOrganizationsByIDs failure paths — consistent with enrichTransactionsFromDB pattern (per copilot-pull-request-reviewer)
  • donation_service.go: Changed _, span :=ctx, span := in projectDonationSummaries and pass returned ctx to downstream lookups for correct trace propagation (per copilot-pull-request-reviewer)
  • models/donation.go: Renamed DonorAvatarDonorAvatarURL consistent with existing AvatarURL naming convention (per copilot-pull-request-reviewer)
  • models/donation.go: Fixed DonationSummary doc comment to accurately describe omitted fields and explicitly note that donor_name/donor_avatar_url are intentionally included display-only fields (per copilot-pull-request-reviewer)
  • donation_service.go / subscription_service.go: Updated validation error messages from amount_in_centsamount_cents to match renamed wire field (per copilot-pull-request-reviewer)
  • clients/ledger.go: Added doc comment documenting the page-alignment constraint on offset/limit+1 conversion (per copilot-pull-request-reviewer)
  • PR description: Removed section 5 (Helm ingress template) — that change was moved to separate PR feat(helm): add missing ingress template to initiatives-service chart (DO NOT MERGE) #52

Declined

  • donation_service.go:61projectDonationSummaries rename suggestion: not a correctness issue; "project" is the standard DDD verb for model → view projection, unrelated to the initiative/project domain concept
  • ledger.go / initiative_handler.go — offset alignment enforcement (400 on non-multiple offsets): all callers use limit-aligned offsets in practice; adding dead validation for an unreachable code path would increase complexity without fixing a real bug. Constraint documented instead.

Threads Resolved

10 of 10 unresolved threads addressed in this iteration.

Copilot AI review requested due to automatic review settings May 26, 2026 06:45

Copilot AI 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.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.

Comment thread frontend/app/composables/initiatives/useInitiativeTransactions.ts
Comment thread backend/internal/service/donation_service.go
computed was used in the enabled option but missing from the vue import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>

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

Frontend changes looks good to me

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

LGTM

@mlehotskylf
mlehotskylf merged commit 6be0139 into main May 26, 2026
9 checks passed
@mlehotskylf
mlehotskylf deleted the fix/api-hardening-pii-pagination-naming branch May 26, 2026 18:43
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.

4 participants