Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(firestore-stripe-payments): Update payments collection for refunds #650

Open
wants to merge 1 commit into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions firestore-stripe-payments/POSTINSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Here's how to set up the webhook and configure your extension to use it:
- `payment_intent.succeeded`
- `payment_intent.canceled`
- `payment_intent.payment_failed`
- `charge.refunded` (optional, will sync refunded payments to Cloud Firestore)
- `tax_rate.created` (optional)
- `tax_rate.updated` (optional)
- `invoice.paid` (optional, will sync invoices to Cloud Firestore)
Expand Down Expand Up @@ -193,6 +194,8 @@ db.collection("${param:PRODUCTS_COLLECTION}")

You can create Checkout Sessions for one-time payments when referencing a one-time price ID. One-time payments will be synced to Cloud Firestore into a payments collection for the relevant customer doc if you update your webhook handler in the Stripe dashboard to include the following events: `payment_intent.succeeded`, `payment_intent.payment_failed`, `payment_intent.canceled`, `payment_intent.processing`.

If a payment is refunded in Stripe the associated payment in the payments collection can be updated. To update the payments collection for refunds add the following events to your webhook handler in the Stripe dashboard: `charge.refunded`.

To create a Checkout Session ID for a one-time payment, pass `mode: 'payment` to the Checkout Session doc creation:

```js
Expand Down
2 changes: 2 additions & 0 deletions firestore-stripe-payments/extension.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,5 @@ events:
description:
Occurs when a PaymentIntent has failed the attempt to create a payment
method or a payment.
- type: com.stripe.v1.charge.refunded
description: Occurs whenever a charge is refunded.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const setupWebhooks = async (url) => {
'payment_intent.succeeded',
'payment_intent.canceled',
'payment_intent.payment_failed',
'charge.refunded',
'tax_rate.created',
'tax_rate.updated',
'invoice.paid',
Expand Down
50 changes: 50 additions & 0 deletions firestore-stripe-payments/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,51 @@ const insertPaymentRecord = async (
logs.firestoreDocCreated('payments', payment.id);
};

/**
* Update the charges for a payment intent.
*/
const updateChargeForPaymentIntent = async (charge: Stripe.Charge) => {
const customersSnap = await admin
.firestore()
.collection(config.customersCollectionPath)
.where('stripeId', '==', charge.customer)
.get();
if (customersSnap.size !== 1) {
throw new Error('User not found!');
}

const paymentId =
typeof charge.payment_intent === 'string'
? charge.payment_intent
: charge.payment_intent.id;

const paymentRef = customersSnap.docs[0].ref
.collection('payments')
.doc(paymentId);

const charges: Stripe.ApiList<Stripe.Charge> = (
await paymentRef.get()
).data().charges ?? {
data: [],
has_more: false,
object: 'list',
url: '/v1/charges',
};

const chargeIndex = charges.data.findIndex(({ id }) => id === charge.id);
if (chargeIndex !== -1) {
charges.data[chargeIndex] = charge;
} else {
charges.data.push(charge);
}

await customersSnap.docs[0].ref
.collection('payments')
.doc(paymentId)
.set({ charges }, { merge: true });
logs.firestoreDocCreated('payments', paymentId);
};

/**
* A webhook handler function for the relevant Stripe events.
*/
Expand Down Expand Up @@ -757,6 +802,7 @@ export const handleWebhookEvents = functions.handler.https.onRequest(
'payment_intent.succeeded',
'payment_intent.canceled',
'payment_intent.payment_failed',
'charge.refunded',
]);
let event: Stripe.Event;

Expand Down Expand Up @@ -857,6 +903,10 @@ export const handleWebhookEvents = functions.handler.https.onRequest(
const paymentIntent = event.data.object as Stripe.PaymentIntent;
await insertPaymentRecord(paymentIntent);
break;
case 'charge.refunded':
const charge = event.data.object as Stripe.Charge;
await updateChargeForPaymentIntent(charge);
break;
default:
logs.webhookHandlerError(
new Error('Unhandled relevant event!'),
Expand Down
Loading