Skip to content

Commit

Permalink
feature(admin-panel): Add nestjs subscription service
Browse files Browse the repository at this point in the history
Because:
- We want to display account subscription data in the admin panel.

This Commit:
- Hooks up the Account React component to real subscription data.

- Fixes a long standing issue with Knex in the admin panel! Knex instance is now bound to BaseAuthModel and repurposed across derived classes.
- Introduces a subscription module that provides the nestjs services necessary to retrieve subscription data.
- Introduces a subscription service that acts as the primary point of request for subscription data.
- Introduces stripe service so that stripe can be dependency injected.
- Introduces firestore service so that firestore can be dependency injected.
- Introduces play store service so that play store accessor can be dependency injected.
- Introduces app store service so that app store accessor can be dependency injected.
- Introduces subscription formatters that unify subscription dtos into standard response format.
- Improves config setup to allow for local.json and secrets.json files to be used.
- Introduces configuration settings so that underlying services can be accessed.
- Introduces feature flags to disable queries to underlying stripe, google or apple apis.
- Hoists a couple joi validators up to fxa-shared for reuse.
- Breaks out an auxiliary method that can determine product ids given iapType and a plan. This exists in in stripe.ts in fxa-shared. (The previous code had zero dependence on the stripe class, which made this possible. This was primarily done for testing purposes.)
- Exposes a couple more fields on AppStoreSubscriptionPurchase to support subscription formatting.
- Fixes typing on MozSubscription.endedAt. Value is allowed to be null | undefined.
- Adds tests and achieves 90+ percent test coverage on all new code.
- Adds reusable mocks for standard services to facilitate testing.
- Updates readme with info about subscriptions, configuration, feature flags, and testing.
- Updates pm2.js with better defaults to support subscription service.
  • Loading branch information
dschom committed May 21, 2022
1 parent 8fada53 commit 158a87b
Show file tree
Hide file tree
Showing 38 changed files with 2,417 additions and 298 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ Thumbs.db
packages/browserid-verifier/loadtest/venv
packages/browserid-verifier/loadtest/*.pyc

# fxa-admin-server
packages/fxa-admin-server/src/config/*.json

# fxa-auth-server
packages/fxa-auth-server/sandbox
packages/fxa-auth-server/config/key.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -487,28 +487,27 @@ export const Account = ({
</Guard>

{/* Temporary check for fake hard-coded value until we fetch actual subscriptions in FXA-4237 */}
{subscriptions &&
subscriptions[0].productName !== 'Cooking with Foxkeh' && (
<>
<li className="account-li">
<h3 className="account-header">Subscriptions</h3>
{subscriptions && (
<>
<li className="account-li">
<h3 className="account-header">Subscriptions</h3>
</li>
{subscriptions && subscriptions.length > 0 ? (
<>
{subscriptions.map((subscription) => (
<Subscription
key={subscription.subscriptionId}
{...subscription}
/>
))}
</>
) : (
<li className="account-li account-border-info">
This account doesn't have any subscriptions.
</li>
{subscriptions && subscriptions.length > 0 ? (
<>
{subscriptions.map((subscription) => (
<Subscription
key={subscription.subscriptionId}
{...subscription}
/>
))}
</>
) : (
<li className="account-li account-border-info">
This account doesn't have any subscriptions.
</li>
)}
</>
)}
)}
</>
)}
</ul>

<hr className="border-grey-50 mb-4" />
Expand Down Expand Up @@ -588,9 +587,12 @@ export const Account = ({
);
};

const getEmailBounceDescription = (bounceType: string, bounceSubType: string) => {
const getEmailBounceDescription = (
bounceType: string,
bounceSubType: string
) => {
let description;
switch(bounceType) {
switch (bounceType) {
case BounceType.Undetermined: {
if (bounceSubType === BounceSubType.Undetermined) {
description = BOUNCE_DESCRIPTIONS.undetermined;
Expand Down Expand Up @@ -623,7 +625,7 @@ const getEmailBounceDescription = (bounceType: string, bounceSubType: string) =>
} else if (bounceSubType === BounceSubType.MessageTooLarge) {
description = BOUNCE_DESCRIPTIONS.transientMessageTooLarge;
} else if (bounceSubType === BounceSubType.ContentRejected) {
description = BOUNCE_DESCRIPTIONS.transientContentRejected
description = BOUNCE_DESCRIPTIONS.transientContentRejected;
} else if (bounceSubType === BounceSubType.AttachmentRejected) {
description = BOUNCE_DESCRIPTIONS.transientAttachmentRejected;
} else {
Expand Down Expand Up @@ -656,7 +658,7 @@ const getEmailBounceDescription = (bounceType: string, bounceSubType: string) =>
}
}
return description.map((paragraph, index) => <p key={index}>{paragraph}</p>);
}
};

const EmailBounce = ({
email,
Expand All @@ -667,16 +669,15 @@ const EmailBounce = ({
diagnosticCode,
}: EmailBounceType) => {
const date = dateFormat(new Date(createdAt), DATE_FORMAT);
const bounceDescription = getEmailBounceDescription(bounceType, bounceSubType);
const bounceDescription = getEmailBounceDescription(
bounceType,
bounceSubType
);
return (
<div className="account-li account-border-info">
<table className="pt-1" aria-label="simple table">
<tbody>
<ResultTableRow
label="email"
value={email}
testId={'bounce-email'}
/>
<ResultTableRow label="email" value={email} testId={'bounce-email'} />
<ResultTableRow
label="template"
value={templateName}
Expand Down Expand Up @@ -890,7 +891,9 @@ const ResultTableRow = ({
}) => {
return (
<tr>
<td className="account-label"><span>{label}</span></td>
<td className="account-label">
<span>{label}</span>
</td>
<td data-testid={testId}>{value ? value : <i>Unknown</i>}</td>
</tr>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const subscription = {
currentPeriodEnd: 1596758906,
currentPeriodStart: 1594080506,
cancelAtPeriodEnd: false,
endAt: 1596759006,
endedAt: 1596759006,
latestInvoice:
'https://pay.stripe.com/invoice/acct_1GCAr3BVqmGyQTMa/invst_HbGuRujVERsyXZy0zArp7SLFRhY9i6S/pdf',
planId: 'plan_GqM9N6qyhvxaVk',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const Subscription = ({
currentPeriodEnd,
currentPeriodStart,
cancelAtPeriodEnd,
endAt,
endedAt,
latestInvoice,
planId,
productName,
Expand All @@ -32,9 +32,11 @@ const Subscription = ({
<li className="account-li">
Created at: <span>{dateFormat(new Date(created), DATE_FORMAT)}</span>
</li>
<li className="account-li">
Ends at: <span>{dateFormat(new Date(endAt), DATE_FORMAT)}</span>
</li>
{endedAt != null && (
<li className="account-li">
Ended at: <span>{dateFormat(new Date(endedAt), DATE_FORMAT)}</span>
</li>
)}
<li className="account-li">
Current period start:{' '}
<span>{dateFormat(new Date(currentPeriodStart), DATE_FORMAT)}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const ACCOUNT_SCHEMA = `
currentPeriodEnd
currentPeriodStart
cancelAtPeriodEnd
endAt
endedAt
latestInvoice
planId
productName
Expand Down
63 changes: 63 additions & 0 deletions packages/fxa-admin-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,69 @@ Use the `--hasDiagnosticCode` flag to create a bounce with a diagnostic code (ot

Example: `yarn email-bounce --email test@example.com --count 3`

## Subscription Service

The subscription service binds account records to info about their current set of subscriptions. Subscription data is held in two datastores. Firestore acts as a backing document store and is responsible for holding documents containing subscription information. If the subscription data cannot be located in Firestore, then the underlying API implementation will be queried and the subscription data will be pulled directly from the source.

It can be difficult to test the subscription in its full form. Stripe integration is not difficult, but workflows for apple app store purchases and google play purchases are difficult to test manually. As a side effect of this, feature flags have been added to disable these calls during local development.

## Feature Flags

Feature flags can be found in `./src/config/index.ts`, under the `featureFlags` section. Feature flags should be named in an obvious way, and are useful for local testing, soft launches, and as a short circuit if a feature starts misbehaving.

## Configuration

All configuration settings can be found in `./src/config/indext.ts`. Furthermore, overrides can be applied by adding json files to this folder containing partial overrides. For example, to create local settings, add the following to `./src/config/local.json`

```
{
featureFlags: {
subscriptions: {
playStore: false
}
}
}
```

## Secrets

With the addition of subscriptions, secrets are now required to fully exercise the subscription service code. Adding secrets is not difficult though. Simply add a `./src/config/secrets.json` file and provide the required config settings. It is generally best to ask a fellow developer to get help with these values, as setting this up yourself can be time consuming.

In the event you can’t provide a secrets file, but still want to do some development work, consider using feature flags to disable subscription features accordingly.

Here is an example secrets.json that would support stripe, and google play, and apple app store.

```
{
"subscriptions": {
"stripeApiKey": "sk-test_123",
"stripeWebhookSecret": "wh-sec_123",
"paypalNvpSigCredentials": {
"enabled": true,
"sandbox": true,
"user": "sb-123.business.example.com",
"pwd": "pwd123",
"signature": "sig--123"
},
"playApiServiceAccount": {
"enabled": true,
"keyFilename": "/Users/me/my-secrets/firestore.json",
"projectId": "test-123"
}
},
"googleAuthConfig": {
"clientSecret": "secret-google-123"
},
"appleAuthConfig": {
"clientSecret": "secret-apple-123"
}
}
```

_(And of course real values would need to be provided…)_

**(Note: There is no watch on .json files, so run a yarn build after changing them.)**

## Testing

This package uses [Jest](https://mochajs.org/) to test its code. By default `yarn test` will test all files ending in `.spec.ts`.
Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-admin-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "FxA GraphQL Admin Server",
"scripts": {
"prebuild": "rimraf dist",
"build": "yarn nest build",
"build": "yarn nest build && cp ./src/config/*.json ./dist/config",
"lint": "eslint *",
"audit": "npm audit --json | audit-filter --nsp-config=.nsprc --audit=-",
"start": "pm2 start pm2.config.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/fxa-admin-server/pm2.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ module.exports = {
max_restarts: '1',
min_uptime: '2m',
env: {
CONFIG_FILES: 'src/config/secrets.json,src/config/local.json',
PATH,
NODE_ENV: 'development',
TS_NODE_TRANSPILE_ONLY: 'true',
TS_NODE_FILES: 'true',
PORT: '8095',
SENTRY_ENV: 'local',
SENTRY_DSN: process.env.SENTRY_DSN_ADMIN_PANEL,
FIRESTORE_EMULATOR_HOST: 'localhost:9090',
},
filter_env: ['npm_'],
watch: ['src'],
Expand Down
9 changes: 5 additions & 4 deletions packages/fxa-admin-server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { GraphQLModule } from '@nestjs/graphql';
import { HealthModule } from 'fxa-shared/nestjs/health/health.module';
import { LoggerModule } from 'fxa-shared/nestjs/logger/logger.module';
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
import { SentryModule } from 'fxa-shared/nestjs/sentry/sentry.module';
import { MetricsFactory } from 'fxa-shared/nestjs/metrics.service';
import { SentryModule } from 'fxa-shared/nestjs/sentry/sentry.module';
import {
createContext,
SentryPlugin,
} from 'fxa-shared/nestjs/sentry/sentry.plugin';
import { getVersionInfo } from 'fxa-shared/nestjs/version';
import { join } from 'path';

import { UserGroupGuard } from './auth/user-group-header.guard';
import Config, { AppConfig } from './config';
import { DatabaseModule } from './database/database.module';
import { DatabaseService } from './database/database.service';
import { GqlModule } from './gql/gql.module';
import { APP_GUARD } from '@nestjs/core';
import { UserGroupGuard } from './auth/user-group-header.guard';
import { SubscriptionModule } from './subscriptions/subscriptions.module';

const version = getVersionInfo(__dirname);

Expand All @@ -32,6 +32,7 @@ const version = getVersionInfo(__dirname);
isGlobal: true,
}),
DatabaseModule,
SubscriptionModule,
GqlModule,
GraphQLModule.forRootAsync({
imports: [ConfigModule, SentryModule],
Expand Down
Loading

0 comments on commit 158a87b

Please sign in to comment.