feat(payments-api): Add billings-and-subscriptions endpoint#20407
feat(payments-api): Add billings-and-subscriptions endpoint#20407david1alvarez wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
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/Serviceplus 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-apiconfig 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.
| export const CurrentUser = createParamDecorator< | ||
| unknown, | ||
| ExecutionContext, | ||
| FxaOAuthUser | ||
| >((_data, context) => context.switchToHttp().getRequest().user); |
| // `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, |
| const priceIds = offering.offering.defaultPurchase.stripePlanChoices.map( | ||
| (choice) => choice.stripePlanChoice | ||
| ); | ||
| const price = await this.priceManager.retrieveByInterval( | ||
| priceIds, | ||
| offering.interval as unknown as SubplatInterval | ||
| ); |
| 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; | ||
| } | ||
|
|
683b9ed to
dcdbb84
Compare
StaberindeZA
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
agreed, removed
| const subscriptions: Subscription[] = []; | ||
|
|
||
| for (const sub of activeStripeSubscriptions) { | ||
| const priceId = sub.items.data[0]?.price.id; |
There was a problem hiding this comment.
issue: use util function getPriceFromSubscription
| } | ||
| } | ||
|
|
||
| private async fetchActiveStripeSubscriptions( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
agreed, moved
| return details; | ||
| } | ||
|
|
||
| private determinePaymentProvider( |
There was a problem hiding this comment.
issue: use subscription.manager > getPaymentProvider instead
| let googleIapPurchaseManager: Mock<GoogleIapPurchaseManager>; | ||
| let logger: { log: jest.Mock; error: jest.Mock }; | ||
|
|
||
| beforeEach(async () => { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = ( |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I didn't see docs for response validation in the NestJS validation docs, only for input validation. Do they support output validation?
There was a problem hiding this comment.
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
| err && | ||
| typeof err === 'object' && | ||
| 'name' in err && | ||
| (err as { name: string }).name === 'AccountCustomerNotFoundError' |
There was a problem hiding this comment.
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
Because:
This commit:
Closes #PAY-3613
Checklist
Put an
xin the boxes that applyHow to review (Optional)
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.