зеркало из https://github.com/mozilla/fxa.git
Merge pull request #14388 from mozilla/FXA-5594
task(admin-panel): Add ability to see canceled/inactive subscriptions
This commit is contained in:
Коммит
3351a003e7
|
@ -16,12 +16,17 @@ import {
|
|||
import { AppStoreService } from './appstore.service';
|
||||
import { PlayStoreService } from './playstore.service';
|
||||
import { StripeService } from './stripe.service';
|
||||
import { SubscriptionsService } from './subscriptions.service';
|
||||
import {
|
||||
SubscriptionsService,
|
||||
VALID_SUBSCRIPTION_STATUSES,
|
||||
} from './subscriptions.service';
|
||||
import { addDays, created } from './test.util';
|
||||
|
||||
describe('Subscription Service', () => {
|
||||
// Stripe Service Mock
|
||||
const mockFetchCustomers = jest.fn();
|
||||
const subscriptionStatusTypes = VALID_SUBSCRIPTION_STATUSES;
|
||||
|
||||
const mockLookupLatestInvoice = jest.fn();
|
||||
const mockAllAbbrevPlans = jest.fn();
|
||||
const mockCreateManageSubscriptionLink = jest.fn();
|
||||
|
@ -145,7 +150,11 @@ describe('Subscription Service', () => {
|
|||
const subscriptions = await service.getSubscriptions(uid);
|
||||
expect(subscriptions).toEqual([]);
|
||||
expect(mockAllAbbrevPlans).toBeCalledTimes(1);
|
||||
expect(mockFetchCustomers).toBeCalledWith(uid, ['subscriptions']);
|
||||
expect(mockFetchCustomers).toBeCalledWith(
|
||||
uid,
|
||||
['subscriptions'],
|
||||
subscriptionStatusTypes
|
||||
);
|
||||
expect(mockAppStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
expect(mockPlayStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
});
|
||||
|
@ -213,7 +222,11 @@ describe('Subscription Service', () => {
|
|||
},
|
||||
]);
|
||||
expect(mockAllAbbrevPlans).toBeCalledTimes(1);
|
||||
expect(mockFetchCustomers).toBeCalledWith(uid, ['subscriptions']);
|
||||
expect(mockFetchCustomers).toBeCalledWith(
|
||||
uid,
|
||||
['subscriptions'],
|
||||
subscriptionStatusTypes
|
||||
);
|
||||
expect(mockCreateManageSubscriptionLink).toBeCalledWith(customerId);
|
||||
expect(mockAppStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
expect(mockPlayStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
|
@ -274,7 +287,11 @@ describe('Subscription Service', () => {
|
|||
},
|
||||
]);
|
||||
expect(mockAllAbbrevPlans).toBeCalledTimes(1);
|
||||
expect(mockFetchCustomers).toBeCalledWith(uid, ['subscriptions']);
|
||||
expect(mockFetchCustomers).toBeCalledWith(
|
||||
uid,
|
||||
['subscriptions'],
|
||||
subscriptionStatusTypes
|
||||
);
|
||||
expect(mockAppStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
expect(mockPlayStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
});
|
||||
|
@ -377,7 +394,11 @@ describe('Subscription Service', () => {
|
|||
},
|
||||
]);
|
||||
expect(mockAllAbbrevPlans).toBeCalledTimes(1);
|
||||
expect(mockFetchCustomers).toBeCalledWith(uid, ['subscriptions']);
|
||||
expect(mockFetchCustomers).toBeCalledWith(
|
||||
uid,
|
||||
['subscriptions'],
|
||||
subscriptionStatusTypes
|
||||
);
|
||||
expect(mockAppStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
expect(mockPlayStoreGetSubscriptions).toBeCalledWith(uid);
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { ConfigService } from '@nestjs/config';
|
||||
import { MozLoggerService } from 'fxa-shared/nestjs/logger/logger.service';
|
||||
import { AbbrevPlan } from 'fxa-shared/subscriptions/types';
|
||||
import Stripe from 'stripe';
|
||||
import { MozSubscription } from '../gql/model/moz-subscription.model';
|
||||
import { AppStoreService } from './appstore.service';
|
||||
import { PlayStoreService } from './playstore.service';
|
||||
|
@ -16,6 +17,20 @@ import {
|
|||
StripeFormatter,
|
||||
} from './subscriptions.formatters';
|
||||
|
||||
/**
|
||||
* List of valid of subscription statuses. This should be all known
|
||||
* stripe subscription types.
|
||||
*/
|
||||
export const VALID_SUBSCRIPTION_STATUSES: Stripe.Subscription.Status[] = [
|
||||
'active',
|
||||
'canceled',
|
||||
'incomplete',
|
||||
'incomplete_expired',
|
||||
'past_due',
|
||||
'trialing',
|
||||
'unpaid',
|
||||
];
|
||||
|
||||
/**
|
||||
* Provides access to account subscriptions
|
||||
*/
|
||||
|
@ -89,9 +104,12 @@ export class SubscriptionsService {
|
|||
return;
|
||||
}
|
||||
|
||||
const customer = await this.stripeService.fetchCustomer(uid, [
|
||||
'subscriptions',
|
||||
]);
|
||||
const customer = await this.stripeService.fetchCustomer(
|
||||
uid,
|
||||
['subscriptions'],
|
||||
VALID_SUBSCRIPTION_STATUSES
|
||||
);
|
||||
|
||||
for (const subscription of customer?.subscriptions?.data || []) {
|
||||
// Inspired by code in auth-server payments ;]
|
||||
const plan = await plans.find(
|
||||
|
|
|
@ -621,28 +621,46 @@ describe('StripeFirestore', () => {
|
|||
});
|
||||
|
||||
describe('retrieveCustomerSubscriptions', () => {
|
||||
it('retrieves customer subscriptions', async () => {
|
||||
const subscriptionSnap = {
|
||||
docs: [{ data: () => ({ ...customer.subscriptions.data[0] }) }],
|
||||
};
|
||||
customerCollectionDbRef.where = sinon.fake.returns({
|
||||
get: sinon.fake.resolves({
|
||||
empty: false,
|
||||
docs: [
|
||||
{
|
||||
ref: {
|
||||
collection: sinon.fake.returns({
|
||||
get: sinon.fake.resolves(subscriptionSnap),
|
||||
}),
|
||||
describe('retrieves customer subscriptions', () => {
|
||||
beforeEach(() => {
|
||||
const subscriptionSnap = {
|
||||
docs: [{ data: () => ({ ...customer.subscriptions.data[0] }) }],
|
||||
};
|
||||
customerCollectionDbRef.where = sinon.fake.returns({
|
||||
get: sinon.fake.resolves({
|
||||
empty: false,
|
||||
docs: [
|
||||
{
|
||||
ref: {
|
||||
collection: sinon.fake.returns({
|
||||
get: sinon.fake.resolves(subscriptionSnap),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('without status filter', async () => {
|
||||
const subscriptions =
|
||||
await stripeFirestore.retrieveCustomerSubscriptions(customer.id);
|
||||
assert.deepEqual(subscriptions, [customer.subscriptions.data[0]]);
|
||||
});
|
||||
|
||||
it('with status filter', async () => {
|
||||
const subscriptions =
|
||||
await stripeFirestore.retrieveCustomerSubscriptions(customer.id, [
|
||||
'active',
|
||||
]);
|
||||
assert.deepEqual(subscriptions, [customer.subscriptions.data[0]]);
|
||||
});
|
||||
|
||||
it('with empty status filter', async () => {
|
||||
const subscriptions =
|
||||
await stripeFirestore.retrieveCustomerSubscriptions(customer.id, []);
|
||||
assert.deepEqual(subscriptions, []);
|
||||
});
|
||||
const subscriptions = await stripeFirestore.retrieveCustomerSubscriptions(
|
||||
customer.id
|
||||
);
|
||||
assert.deepEqual(subscriptions, [customer.subscriptions.data[0]]);
|
||||
});
|
||||
|
||||
it('retrieves only active customer subscriptions', async () => {
|
||||
|
|
|
@ -6275,7 +6275,8 @@ describe('StripeHelper', () => {
|
|||
);
|
||||
sinon.assert.calledOnceWithExactly(
|
||||
stripeHelper.stripeFirestore.retrieveCustomerSubscriptions,
|
||||
customer.id
|
||||
customer.id,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -6298,7 +6299,8 @@ describe('StripeHelper', () => {
|
|||
);
|
||||
sinon.assert.calledOnceWithExactly(
|
||||
stripeHelper.stripeFirestore.retrieveCustomerSubscriptions,
|
||||
customer.id
|
||||
customer.id,
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -258,8 +258,15 @@ export class StripeFirestore {
|
|||
|
||||
/**
|
||||
* Retrieve all the customer subscriptions from Firestore.
|
||||
* @param customerId - The target customer
|
||||
* @param statusFilter - Optional list of subscription statuses to filter by. Only
|
||||
* subscriptions with status contained in this list will be
|
||||
* returned. Defaults to ACTIVE_SUBSCRIPTION_STATUSES.
|
||||
*/
|
||||
async retrieveCustomerSubscriptions(customerId: string) {
|
||||
async retrieveCustomerSubscriptions(
|
||||
customerId: string,
|
||||
statusFilter: Stripe.Subscription.Status[] = ACTIVE_SUBSCRIPTION_STATUSES
|
||||
) {
|
||||
const customerSnap = await this.customerCollectionDbRef
|
||||
.where('id', '==', customerId)
|
||||
.get();
|
||||
|
@ -275,7 +282,7 @@ export class StripeFirestore {
|
|||
.get();
|
||||
return subscriptionSnap.docs
|
||||
.map((doc) => doc.data() as Stripe.Subscription)
|
||||
.filter((sub) => ACTIVE_SUBSCRIPTION_STATUSES.includes(sub.status));
|
||||
.filter((sub) => statusFilter.includes(sub.status));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -208,7 +208,8 @@ export abstract class StripeHelper {
|
|||
| 'subscriptions'
|
||||
| 'invoice_settings.default_payment_method'
|
||||
| 'tax'
|
||||
)[]
|
||||
)[],
|
||||
statusFilter?: Stripe.Subscription.Status[]
|
||||
): Promise<Stripe.Customer | void> {
|
||||
const { stripeCustomerId } = (await getAccountCustomerByUid(uid)) || {};
|
||||
if (!stripeCustomerId) {
|
||||
|
@ -218,7 +219,8 @@ export abstract class StripeHelper {
|
|||
// By default this has subscriptions expanded.
|
||||
let customer = await this.expandResource<Stripe.Customer>(
|
||||
stripeCustomerId,
|
||||
CUSTOMER_RESOURCE
|
||||
CUSTOMER_RESOURCE,
|
||||
statusFilter
|
||||
);
|
||||
|
||||
if (customer.deleted) {
|
||||
|
@ -468,7 +470,8 @@ export abstract class StripeHelper {
|
|||
*/
|
||||
async expandResource<T>(
|
||||
resource: string | T,
|
||||
resourceType: typeof VALID_RESOURCE_TYPES[number]
|
||||
resourceType: typeof VALID_RESOURCE_TYPES[number],
|
||||
statusFilter?: Stripe.Subscription.Status[]
|
||||
): Promise<T> {
|
||||
if (typeof resource !== 'string') {
|
||||
return resource;
|
||||
|
@ -493,7 +496,10 @@ export abstract class StripeHelper {
|
|||
return customer;
|
||||
}
|
||||
const subscriptions =
|
||||
await this.stripeFirestore.retrieveCustomerSubscriptions(resource);
|
||||
await this.stripeFirestore.retrieveCustomerSubscriptions(
|
||||
resource,
|
||||
statusFilter
|
||||
);
|
||||
(customer as any).subscriptions = {
|
||||
data: subscriptions as any,
|
||||
has_more: false,
|
||||
|
|
Загрузка…
Ссылка в новой задаче