Skip to content

Commit e97216a

Browse files
authored
fix(firestore-stripe-payments): added subscription payments prices, test and refactor included (#393)
* fix(firestore-stripe-payments): added subscription payments prices, tests and refactor included * chore: locked firebase tools workflow to 10.9.2 * added setup emulator for collection helper functions * chore: commiting empty env files * chore: updated ci firebase tools to latest * pinned sinon types at 10.0.6
1 parent b93e13a commit e97216a

File tree

17 files changed

+376
-88
lines changed

17 files changed

+376
-88
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
STRIPE_API_KEY=
1+
STRIPE_API_KEY=
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as admin from 'firebase-admin';
2+
import { DocumentReference, DocumentData } from '@google-cloud/firestore';
3+
import {
4+
waitForDocumentToExistInCollection,
5+
waitForDocumentToExistWithField,
6+
} from './utils';
7+
import { UserRecord } from 'firebase-functions/v1/auth';
8+
import setupEmulator from './setupEmulator';
9+
10+
if (admin.apps.length === 0) {
11+
admin.initializeApp({ projectId: 'demo-project' });
12+
}
13+
14+
setupEmulator();
15+
16+
const firestore = admin.firestore();
17+
18+
function customerCollection() {
19+
return firestore.collection('customers');
20+
}
21+
22+
function paymentsCollection(userId) {
23+
return firestore.collection('customers').doc(userId).collection('payments');
24+
}
25+
26+
export async function findCustomerInCollection(user: UserRecord) {
27+
const doc = firestore.collection('customers').doc(user.uid);
28+
29+
const customerDoc = await waitForDocumentToExistWithField(
30+
doc,
31+
'stripeId',
32+
60000
33+
);
34+
35+
return Promise.resolve({ docId: user.uid, ...customerDoc.data() });
36+
}
37+
38+
export async function findCustomerPaymentInCollection(
39+
userId: string,
40+
stripeId: string
41+
) {
42+
const paymentDoc: DocumentData = await waitForDocumentToExistInCollection(
43+
paymentsCollection(userId),
44+
'customer',
45+
stripeId
46+
);
47+
48+
const paymentRef = paymentsCollection(userId).doc(paymentDoc.doc.id);
49+
50+
const updatedPaymentDoc = await waitForDocumentToExistWithField(
51+
paymentRef,
52+
'prices'
53+
);
54+
55+
return updatedPaymentDoc.data();
56+
}
57+
58+
export async function createCheckoutSession(userId, subscription) {
59+
const checkoutSessionCollection = customerCollection()
60+
.doc(userId)
61+
.collection('checkout_sessions');
62+
63+
const checkoutSessionDocument: DocumentReference =
64+
await checkoutSessionCollection.add({
65+
success_url: 'http://test.com/success',
66+
cancel_url: 'http://test.com/cancel',
67+
...subscription,
68+
});
69+
70+
const checkoutSessionDoc = await waitForDocumentToExistWithField(
71+
checkoutSessionDocument,
72+
'created'
73+
);
74+
75+
return checkoutSessionDoc.data();
76+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import puppeteer from 'puppeteer';
2+
3+
export default async (url: string): Promise<void> => {
4+
console.info('Running checkout form in headless mode...');
5+
const browser = await puppeteer.launch();
6+
try {
7+
const page = await browser.newPage();
8+
await page.goto(url, {
9+
waitUntil: 'networkidle0',
10+
timeout: 120000,
11+
});
12+
13+
await page.focus('#cardNumber');
14+
await page.keyboard.type('4242424242424242', { delay: 100 });
15+
await page.keyboard.press('Enter');
16+
17+
await page.focus('#cardExpiry');
18+
await page.keyboard.type('1224');
19+
20+
await page.focus('#cardCvc');
21+
await page.keyboard.type('123');
22+
23+
await page.focus('#billingName');
24+
await page.keyboard.type('testing');
25+
26+
await page.focus('#billingAddressLine1');
27+
await page.keyboard.type('1600 Amphitheatre Parkwa');
28+
await page.keyboard.press('Enter');
29+
30+
await page.focus('#billingLocality');
31+
await page.keyboard.type('Mountain View');
32+
33+
await page.focus('#billingPostalCode');
34+
await page.keyboard.type('CA 94043');
35+
await page.keyboard.press('Enter');
36+
37+
await page.waitForNetworkIdle();
38+
await browser.close();
39+
} catch (exception) {
40+
} finally {
41+
await browser.close();
42+
}
43+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { faker } from '@faker-js/faker';
2+
import config from '../../../lib/config';
3+
import { Product, Subscription } from '../../../src/interfaces';
4+
5+
const stripe = require('stripe')(config.stripeSecretKey);
6+
7+
export const createRandomSubscription = async (
8+
customer
9+
): Promise<Subscription> => {
10+
const name = faker.commerce.product();
11+
12+
/** create a product */
13+
const product: Product = await stripe.products.create({
14+
name,
15+
description: `Description for ${name}`,
16+
});
17+
18+
/** create a price */
19+
const price = await stripe.prices.create({
20+
unit_amount: 1000,
21+
currency: 'gbp',
22+
recurring: { interval: 'month' },
23+
product: product.id,
24+
});
25+
26+
/** create payment method */
27+
const paymentMethod = await stripe.paymentMethods.create({
28+
type: 'card',
29+
card: {
30+
number: '4242424242424242',
31+
exp_month: 5,
32+
exp_year: 2023,
33+
cvc: '314',
34+
},
35+
});
36+
37+
/** attach payment method to customer */
38+
await stripe.paymentMethods.attach(paymentMethod.id, { customer });
39+
await stripe.customers.update(customer, {
40+
invoice_settings: { default_payment_method: paymentMethod.id },
41+
});
42+
43+
/** Create a product */
44+
const subscription: Subscription = await stripe.subscriptions.create({
45+
customer,
46+
items: [{ price: price.id }],
47+
payment_settings: {
48+
payment_method_types: ['card'],
49+
},
50+
});
51+
52+
return Promise.resolve(subscription);
53+
};

firestore-stripe-payments/functions/__tests__/helpers/utils.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export const waitForDocumentToExistWithField = (
3030
let timedOut = false;
3131
const timer = setTimeout(() => {
3232
timedOut = true;
33-
reject(new Error('Timeout waiting for firestore document'));
33+
reject(
34+
new Error(
35+
`Timeout waiting for firestore document to exist with field ${field}`
36+
)
37+
);
3438
}, timeout);
3539
const unsubscribe = document.onSnapshot(async (snapshot: DocumentData) => {
3640
if (snapshot.exists && snapshot.data()[field]) {
@@ -54,7 +58,11 @@ export const waitForDocumentUpdate = (
5458
let timedOut = false;
5559
const timer = setTimeout(() => {
5660
timedOut = true;
57-
reject(new Error('Timeout waiting for firestore document'));
61+
reject(
62+
new Error(
63+
`Timeout waiting for firestore document to update with ${field}`
64+
)
65+
);
5866
}, timeout);
5967
const unsubscribe = document.onSnapshot(async (snapshot: DocumentData) => {
6068
if (snapshot.exists && snapshot.data()[field] === value) {
@@ -78,8 +86,13 @@ export const waitForDocumentToExistInCollection = (
7886
let timedOut = false;
7987
const timer = setTimeout(() => {
8088
timedOut = true;
81-
reject(new Error('Timeout waiting for firestore document'));
89+
reject(
90+
new Error(
91+
`Timeout waiting for firestore document to exist with field ${field} in collection`
92+
)
93+
);
8294
}, timeout);
95+
8396
const unsubscribe = query.onSnapshot(async (snapshot) => {
8497
const docs = snapshot.docChanges();
8598

firestore-stripe-payments/functions/__tests__/createCheckoutSession.test.ts renamed to firestore-stripe-payments/functions/__tests__/tests/checkoutsessions/createCheckoutSession.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as admin from 'firebase-admin';
22
import { DocumentReference, DocumentData } from '@google-cloud/firestore';
33
import { UserRecord } from 'firebase-functions/v1/auth';
4-
import setupEmulator from './helpers/setupEmulator';
5-
import { generateRecurringPrice } from './helpers/setupProducts';
4+
import setupEmulator from '../../helpers/setupEmulator';
5+
import { generateRecurringPrice } from '../../helpers/setupProducts';
66
import {
77
createFirebaseUser,
88
waitForDocumentToExistInCollection,
99
waitForDocumentToExistWithField,
10-
} from './helpers/utils';
10+
} from '../../helpers/utils';
1111

1212
admin.initializeApp({ projectId: 'demo-project' });
1313
setupEmulator();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as admin from 'firebase-admin';
2+
import runCheckout from '../../helpers/forms/runCheckout';
3+
4+
import { UserRecord } from 'firebase-functions/v1/auth';
5+
import { Subscription } from '../../../src/interfaces';
6+
import setupEmulator from '../../helpers/setupEmulator';
7+
import { createRandomSubscription } from '../../helpers/stripeApi/subscriptions';
8+
import { createFirebaseUser } from '../../helpers/utils';
9+
10+
import {
11+
findCustomerInCollection,
12+
createCheckoutSession,
13+
findCustomerPaymentInCollection,
14+
} from '../../helpers/collections';
15+
16+
if (admin.apps.length === 0) {
17+
admin.initializeApp({ projectId: 'demo-project' });
18+
}
19+
20+
setupEmulator();
21+
22+
describe('createSubscriptionCheckoutSession', () => {
23+
let user: UserRecord;
24+
25+
beforeEach(async () => {
26+
user = await createFirebaseUser();
27+
});
28+
29+
afterEach(async () => {
30+
await admin.auth().deleteUser(user.uid);
31+
});
32+
33+
describe('using a web client', () => {
34+
test('successfully creates a subscription based checkout session', async () => {
35+
/** find the customer document */
36+
const { docId, stripeId } = await findCustomerInCollection(user);
37+
38+
/** create a new subscription */
39+
const stripeSubscription: Subscription = await createRandomSubscription(
40+
stripeId
41+
);
42+
43+
/** create a new checkout session */
44+
const { client, success_url, url } = await createCheckoutSession(docId, {
45+
line_items: [
46+
{
47+
//@ts-ignore
48+
price: stripeSubscription.items.data[0].price.id,
49+
quantity: 1,
50+
},
51+
],
52+
});
53+
54+
expect(client).toBe('web');
55+
expect(success_url).toBe('http://test.com/success');
56+
57+
/** complete the checkout fortm */
58+
await runCheckout(url);
59+
60+
/** find user payment */
61+
const { prices } = await findCustomerPaymentInCollection(docId, stripeId);
62+
63+
/** extract prices from array */
64+
const priceRef = await prices[0].get();
65+
const price = priceRef.id;
66+
67+
/** assert values */
68+
//@ts-ignore
69+
expect(price).toEqual(stripeSubscription.items.data[0].price.id);
70+
});
71+
});
72+
});

firestore-stripe-payments/functions/__tests__/createCustomer.test.ts renamed to firestore-stripe-payments/functions/__tests__/tests/customers/createCustomer.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import * as admin from 'firebase-admin';
22
import { DocumentData } from '@google-cloud/firestore';
33

4-
import setupEmulator from './helpers/setupEmulator';
4+
import setupEmulator from '../../helpers/setupEmulator';
55
import { UserRecord } from 'firebase-functions/v1/auth';
66
import {
77
createFirebaseUser,
88
waitForDocumentToExistInCollection,
9-
} from './helpers/utils';
9+
} from '../../helpers/utils';
1010

1111
admin.initializeApp({ projectId: 'demo-project' });
1212
setupEmulator();

firestore-stripe-payments/functions/__tests__/customerDataDeleted.test.ts renamed to firestore-stripe-payments/functions/__tests__/tests/customers/customerDataDeleted.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import * as admin from 'firebase-admin';
22
import { DocumentData } from '@google-cloud/firestore';
3-
import setupEmulator from './helpers/setupEmulator';
4-
import { findCustomer } from './helpers/stripeApi/customers';
3+
import setupEmulator from './../../helpers/setupEmulator';
4+
import { findCustomer } from './../../helpers/stripeApi/customers';
55
import {
66
repeat,
77
waitForDocumentToExistWithField,
88
waitForDocumentToExistInCollection,
99
createFirebaseUser,
10-
} from './helpers/utils';
10+
} from './../../helpers/utils';
1111
import { UserRecord } from 'firebase-functions/v1/auth';
1212

1313
admin.initializeApp({ projectId: 'demo-project' });

firestore-stripe-payments/functions/__tests__/createPortalLink.test.ts renamed to firestore-stripe-payments/functions/__tests__/tests/portalLinks/createPortalLink.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as admin from 'firebase-admin';
22
import { DocumentData } from '@google-cloud/firestore';
33
import functions from 'firebase-functions-test';
4-
import * as cloudFunctions from '../src';
5-
import setupEmulator from './helpers/setupEmulator';
4+
import * as cloudFunctions from '../../../src';
5+
import setupEmulator from '../../helpers/setupEmulator';
66

77
import {
88
createFirebaseUser,
99
waitForDocumentToExistInCollection,
1010
waitForDocumentToExistWithField,
11-
} from './helpers/utils';
11+
} from '../../helpers/utils';
1212
import { UserRecord } from 'firebase-functions/v1/auth';
1313

1414
const testEnv = functions({ projectId: 'demo-project' });

0 commit comments

Comments
 (0)