Skip to content

Commit

Permalink
bundling script
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Stewart Gibson committed Nov 17, 2022
1 parent bf4fcc9 commit fbbb350
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 34 deletions.
216 changes: 216 additions & 0 deletions dist/0.1.0/stripe-integration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
'use strict';

var stripe = require('stripe');

const checkoutCompleteEvent = "checkout.session.completed";

const subscriptionEvents = Object.freeze([
"customer.subscription.created",
"customer.subscription.updated",
"customer.subscription.deleted"
]);

const activeSubscriptionStatuses = Object.freeze([
"trialing",
"active"
]);

const WEEK = 1000 * 60 * 60 * 24 * 7;

async function processStripeEventData(
stripeEvent,
{ customers, key, errors, accounts, products, logger }
) {

try {

logger.debug("Processing stripe event", { id: stripeEvent.id, type: stripeEvent.type });

// if given, map account to customer
await recordCustomerAccountMappings(stripeEvent, { customers, logger });

// if subscription related - update the account subscription
await recordAccountSubscriptionChange(stripeEvent, { customers, key, accounts, products, logger });

} catch (err) {

await errors.doc(stripeEvent.id).set({
event: stripeEvent,
message: err.stack,
when: Date.now()
});
logger.error(err);

}

}

async function recordAccountSubscriptionChange(stripeEvent, { key, customers, accounts, products, logger }) {

if (!stripeEvent.type.includes("subscription")) return;

const customer = stripeEvent.data.object.customer;
const account = (await customers.doc(customer).get()).data()?.account;
if (!account) throw new Error(`Account not found for customer ${customer}`);

if (subscriptionEvents.includes(stripeEvent.type)) {

const ref = accounts.doc(account);
const snapshot = await ref.get();
const data = snapshot.exists ? snapshot.data() : {};
const subscriptions = data.subscriptions || {};
const { id, status, livemode, quantity } = stripeEvent.data.object;
const subscriptionData = subscriptions[id] || {};

if (!subscriptionData.eventDate || (subscriptionData.eventDate < stripeEvent.created)) {

subscriptionData.eventDate = stripeEvent.created;
subscriptionData.status = status;
subscriptionData.quantity = quantity;
subscriptionData.livemode = livemode;

const productSnapshot = await ensureProductDetails(stripeEvent, { key, products, logger });
subscriptionData.product = productSnapshot.data;

logger.log("Merging in subscription", { id, account });
await ref.set({
subscriptions: {
[id]: subscriptionData
}
}, {
merge: true
});

}

} else {

throw new Error(`Unknown subscription event type: ${stripeEvent.type} for event ${stripeEvent.id}`);

}

}


async function recordCustomerAccountMappings(stripeEvent, { customers, logger }) {

if (stripeEvent.type !== checkoutCompleteEvent) return;
const { customer, client_reference_id } = stripeEvent.data.object;
if (customer && client_reference_id) {

const customerRef = customers.doc(customer);
const customerSnapshot = await customerRef.get();
if (!customerSnapshot.exists) {

logger.log("Recording map of customer to account", { customer, account: client_reference_id });
await customerRef.set({ account: client_reference_id, event: stripeEvent.id });

}

}

}

async function ensureProductDetails(stripeEvent, { key, products, productStaleness = WEEK, logger }) {

if (!subscriptionEvents.includes(stripeEvent.type)) return;

const product = stripeEvent.data.object?.plan?.product;
if (!product)
throw new Error(`Product not found in stripe event ${stripeEvent.id} (.data.object.plan.product)`);

const ref = products.doc(product);
const snapshot = await ref.get();
const snapshotData = snapshot.exists && snapshot.data();
if (snapshotData && ((Date.now() - snapshotData.updated) < productStaleness))
return snapshotData;

const client = stripe(key.value());
const resp = await client.products.retrieve(product);
const { description, livemode, name, metadata: { "product-code": code } } = resp;

const updatedData = {
updated: Date.now(),
data: { description, livemode, name, code }
};

logger.log("Setting product because of event", { event: stripeEvent.id, product });
await ref.set(updatedData);
return updatedData;

}

async function processStripeEvent({

request, key, secret, events, customers, accounts, products, errors, logger

}) {

logger.log("Received stripe event", { id: request.body?.id, type: request.body?.type });

// verify the event
const stripeEvent = await buildVerifiedEvent(request, { key, secret });

// record the event
const eventDocument = events.doc(stripeEvent.id);
await eventDocument.set(stripeEvent);
logger.debug("Recorded stripe event", { id: stripeEvent.id });

// process it
await processStripeEventData(stripeEvent, { customers, key, errors, accounts, products, logger });

}
async function buildVerifiedEvent(request, { key, secret }) {

const signature = request.headers["stripe-signature"];
const { webhooks } = stripe(key.value());
// verify and decode
const rawBody = request.rawBody.toString();
return await webhooks.constructEventAsync(rawBody, signature, secret.value());

}

async function replayEvents({

key, events, customers, errors, accounts, products, logger

}) {

const snapshot = await events.get();
const docs = snapshot.docs;
for (const doc of docs) {

// fetch event
const stripeEvent = doc.data();
logger.log("--- Replaying event ---", { id: stripeEvent.id, type: stripeEvent.type });

// process it
await processStripeEventData(stripeEvent, { key, accounts, customers, products, errors, logger });

}

}

async function getActiveSubscriptions({

account, accounts, logger

}) {

const ref = accounts.doc(account);
const snapshot = await ref.get();
if (snapshot.exists) {

const data = snapshot.data();
if ("subscriptions" in data)
return Object.values(data.subscriptions).filter(sub => activeSubscriptionStatuses.includes(sub.status));

}
logger.warn("Account or subscriptions not found", { account });
return [];

}

exports.getActiveSubscriptions = getActiveSubscriptions;
exports.processStripeEvent = processStripeEvent;
exports.replayEvents = replayEvents;
5 changes: 3 additions & 2 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
"logs": "firebase functions:log",
"bundle": "rollup --format cjs ../src/index.js --file ../dist/0.1.0/stripe-integration.js; cp ../dist/0.1.0/stripe-integration.js ."
},
"engines": {
"node": "16"
Expand All @@ -22,4 +23,4 @@
"firebase-functions-test": "^0.2.0"
},
"private": true
}
}
Loading

0 comments on commit fbbb350

Please sign in to comment.