Skip to content

feat(payments-api): Add billings-and-subscriptions endpoint#20407

Open
david1alvarez wants to merge 1 commit intomainfrom
PAY-3613
Open

feat(payments-api): Add billings-and-subscriptions endpoint#20407
david1alvarez wants to merge 1 commit intomainfrom
PAY-3613

Conversation

@david1alvarez
Copy link
Copy Markdown
Contributor

@david1alvarez david1alvarez commented Apr 20, 2026

Because:

  • The payments-api service is intended to provide RPs with information about the subscriptions etc., and should contain the legacy billings-and-subscriptions endpoint

This commit:

  • Adds in the versioned endpoint, and wraps it with an OAuth guard

Closes #PAY-3613

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on:
  • Suggested review order:
  • Risky or complex parts:

Screenshots (Optional)

Please attach the screenshots of the changes made in case of change in user interface.

Other information (Optional)

Any other information that is important to this pull request.

Copilot AI review requested due to automatic review settings April 20, 2026 20:43
@david1alvarez david1alvarez requested a review from a team as a code owner April 20, 2026 20:43
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new OAuth-protected /v1/billing-and-subscriptions endpoint to payments-api that aggregates Stripe billing details + Stripe subscriptions + Apple/Google IAP subscriptions, filtered by OAuth client capabilities.

Changes:

  • Introduces BillingAndSubscriptionsController/Service plus DTOs and Jest coverage for the new endpoint.
  • Adds a @CurrentUser() Nest param decorator to extract the OAuth user from the request.
  • Wires IAP client configuration into payments-api config and module providers, and exports needed IAP purchase classes from the IAP lib.

Reviewed changes

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

Show a summary per file
File Description
libs/payments/iap/src/index.ts Exposes App/Play subscription purchase classes from the IAP library for downstream typing/use.
libs/payments/auth/src/lib/current-user.decorator.ts Adds @CurrentUser() param decorator for controllers to access the authenticated OAuth user.
libs/payments/auth/src/index.ts Re-exports the new CurrentUser decorator.
apps/payments/api/src/config/index.ts Extends RootConfig to include Apple/Google IAP client config sections.
apps/payments/api/src/app/billing-and-subscriptions/billing-and-subscriptions.service.ts Implements the orchestration logic for Stripe + IAP subscription aggregation and capability filtering.
apps/payments/api/src/app/billing-and-subscriptions/billing-and-subscriptions.service.spec.ts Unit tests for the service behavior (Stripe-only, IAP-only, filtering, error sanitization).
apps/payments/api/src/app/billing-and-subscriptions/billing-and-subscriptions.dto.ts Defines response DTOs and validators for the endpoint payload.
apps/payments/api/src/app/billing-and-subscriptions/billing-and-subscriptions.dto.spec.ts Validator-focused tests for the DTOs.
apps/payments/api/src/app/billing-and-subscriptions/billing-and-subscriptions.controller.ts Adds the /v1/billing-and-subscriptions guarded controller endpoint.
apps/payments/api/src/app/billing-and-subscriptions/billing-and-subscriptions.controller.spec.ts Unit tests for guard presence and controller delegation.
apps/payments/api/src/app/app.module.ts Registers controller/service and IAP-related providers into the payments API Nest module.
apps/payments/api/.env Adds local env placeholders for Apple/Google IAP client configuration.

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

Comment on lines +9 to +13
export const CurrentUser = createParamDecorator<
unknown,
ExecutionContext,
FxaOAuthUser
>((_data, context) => context.switchToHttp().getRequest().user);
Comment on lines +364 to +368
// `currency` is a private field on AppStoreSubscriptionPurchase but is
// present on instances returned by the Iap manager; read it through an
// index access to stay off the type boundary until the upstream class
// exposes a public accessor.
currency: (p as unknown as { currency?: string }).currency,
Comment on lines +386 to +392
const priceIds = offering.offering.defaultPurchase.stripePlanChoices.map(
(choice) => choice.stripePlanChoice
);
const price = await this.priceManager.retrieveByInterval(
priceIds,
offering.interval as unknown as SubplatInterval
);
Comment on lines +357 to +374
const storeEntries: Array<{ storeId: string; currency?: string | null }> = [
...googlePurchases.map((p) => ({
storeId: p.sku,
currency: p.priceCurrencyCode,
})),
...applePurchases.map((p) => ({
storeId: p.productId,
// `currency` is a private field on AppStoreSubscriptionPurchase but is
// present on instances returned by the Iap manager; read it through an
// index access to stay off the type boundary until the upstream class
// exposes a public accessor.
currency: (p as unknown as { currency?: string }).currency,
})),
];
if (storeEntries.length === 0) {
return map;
}

@david1alvarez david1alvarez force-pushed the PAY-3613 branch 6 times, most recently from 683b9ed to dcdbb84 Compare April 20, 2026 23:34
Copy link
Copy Markdown
Contributor

@StaberindeZA StaberindeZA left a comment

Choose a reason for hiding this comment

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

I was able to successfully test this and it works as expected.

I've left a bunch of inline comments highlighting a few issues that should be addressed.

* This service is responsible for the `/v1/billing-and-subscriptions` endpoint in the Payments API
*/
@Injectable()
export class BillingAndSubscriptionsService {
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.

issue: please move all private methods into functions and add unit tests for each

This is a pattern that's been followed in other libs/* libraries and services as well.

Comment on lines +110 to +119
const capabilityCache = new Map<string, Record<string, string[]>>();
const isVisibleToClient = async (priceId: string) => {
let clients = capabilityCache.get(priceId);
if (!clients) {
clients =
await this.capabilityManager.priceIdsToClientCapabilities([priceId]);
capabilityCache.set(priceId, clients);
}
return clientId in clients;
};
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.

issue: remove capabilityCache

This seems unnecessary. A customer can only be subscribed to a price once, so as far as I can tell this will never hit the cache and always call capabilityManager.priceIdsToClientCapabilities.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed, removed

const subscriptions: Subscription[] = [];

for (const sub of activeStripeSubscriptions) {
const priceId = sub.items.data[0]?.price.id;
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.

issue: use util function getPriceFromSubscription

}
}

private async fetchActiveStripeSubscriptions(
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.

suggestion: move this to a method on the subscriptionManager

Fetching active subscriptions seems like something that could be reused and might make sense having as a dedicated method on the manager.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed, moved

return details;
}

private determinePaymentProvider(
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.

issue: use subscription.manager > getPaymentProvider instead

let googleIapPurchaseManager: Mock<GoogleIapPurchaseManager>;
let logger: { log: jest.Mock; error: jest.Mock };

beforeEach(async () => {
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.

issue: reuse existing patterns

This beforeEach differs from existing patterns used in the majority modules in libs/payments/*

suggestion: suggest new pattern to team

If there are advantages to this new pattern could you pitch it to the team for further discussion, listing advantages/disadvantages, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The existing pattern is very heavy (mocks vs actual initialization). Claude was pretty convinced we should be mocking these rather the creating a test module, but that's an out-of-scope conversation :)

const CLIENT_ID = 'client_xyz';
const STRIPE_CUSTOMER_ID = 'cus_abc';

const IapOfferingFactory = (
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.

issue: these should be in a *.factories.ts

* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import { z } from 'zod';
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.

issue: DTO's to be used in response validation (and request validation in future when necessary)

Currently auth-server uses joi objects to validate response payloads. The Jira ticket states that the joi objects should be rewritten into class-validator DTOs.

Although it's not stated in the Jira ticket, the intention was to use the class-validator DTOs in NestJS interceptors to validate the response payload.

I'd recommend, for this purpose, that we continue using class-validator since it is the recommended library for NestJS, and brings along some additional advantages such as easy integration with Swagger for example for documentation.

I'm open for further discussion though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I didn't see docs for response validation in the NestJS validation docs, only for input validation. Do they support output validation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was following the request of @julianpoy. I had initially written this up using class-validator, and opted for zod at their recommendation. I'm open to either approach

Comment on lines +183 to +186
err &&
typeof err === 'object' &&
'name' in err &&
(err as { name: string }).name === 'AccountCustomerNotFoundError'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Our normal pattern for error validation is using instanceof, so err instanceof AccountCustomerNotFoundError. This is fragile, since changing the name of the error would break this check with no Typescript failure.

Because:

* The payments-api service is intended to provide RPs with information about the subscriptions etc., and should contain the legacy billings-and-subscriptions endpoint

This commit:

* Adds in the versioned endpoint, and wraps it with an OAuth guard

Closes #PAY-3613
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