I have written the following web hook to capture events coming from stripe after a user has amended a subscription:
export const stripeWebhookEvents = functions.https.onRequest( async ( request: any, response: any ) => {
// Retrieve the event by verifying the signature using the raw body and secret.
let event;
const signature = request.headers["stripe-signature"];
try {
event = stripe.webhooks.constructEvent(
request.rawBody,
signature,
STRIPE_WEBHOOK
);
} catch (err) {
console.log("⚠️ Webhook signature verification failed.", err);
return response.sendStatus(400);
}
// Extract the object from the event.
const eventType = event.type;
console.log(`Scripe event type: ${eventType}.`);
// Handle the event
switch (eventType) {
case "customer.subscription.created": {
// Occurs whenever a customer is signed up for a new plan.
console.log("customer.subscription.created");
try {
const session = event.data.object;
console.log("stripe customer: ", session.customer);
const userRecord = await getUserWithStripeCustomerID(session.customer);
console.log("userRecord: ", userRecord);
const data = {
"subscription_status": session.status,
};
await updateUser(userRecord, data);
} catch (error) {
console.log(error);
}
break;
}
case "customer.subscription.updated": {
// Occurs whenever a subscription changes (e.g., switching from one plan to another,
// or changing the status from trial to active).
break;
}
case "customer.subscription.deleted": {
// The subscription has been deleted so update the user document so that it shows
// that the subscription is inactive.
}
case "invoice.payment_failed": {
// The payment failed or the customer does not have a valid payment method.
// The subscription becomes past_due. Notify your customer and send them to the
// customer portal to update their payment information.
// update the user document so that it shows that the subscription is inactive.
break;
}
default: {
// Unexpected event type
console.log(`Unhandled event type ${eventType}.`);
}
}
response.sendStatus(200);
});
The subscription object that is received from stripe is:
{
"id": "sub_JhxDD458se1T1c",
"object": "subscription",
"application_fee_percent": null,
"automatic_tax": {
"enabled": false
},
"billing_cycle_anchor": 1624221102,
"billing_thresholds": null,
"cancel_at": null,
"cancel_at_period_end": false,
"canceled_at": null,
"collection_method": "charge_automatically",
"created": 1624221102,
"current_period_end": 1626813102,
"current_period_start": 1624221102,
"customer": "cus_FJ9HCZk3rCSlxD",
"days_until_due": null,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"discount": null,
"ended_at": null,
"items": {
"object": "list",
"data": [
{
"id": "si_JhxDPLJExCLmnu",
"object": "subscription_item",
"billing_thresholds": null,
"created": 1624221103,
"metadata": {},
"price": {
"id": "price_1J3q2zHx6drz9dmy2bMdkXOs",
"object": "price",
"active": true,
"billing_scheme": "per_unit",
"created": 1624054821,
"currency": "gbp",
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"product": "prod_JhEWBSIjTi2nlt",
"recurring": {
"aggregate_usage": null,
"interval": "month",
"interval_count": 1,
"usage_type": "licensed"
},
"tax_behavior": "unspecified",
"tiers_mode": null,
"transform_quantity": null,
"type": "recurring",
"unit_amount": 1299,
"unit_amount_decimal": "1299"
},
"quantity": 1,
"subscription": "sub_JhxDD458se1T1c",
"tax_rates": []
}
],
"has_more": false,
"url": "/v1/subscription_items?subscription=sub_JhxDD458se1T1c"
},
"latest_invoice": null,
"livemode": false,
"metadata": {},
"next_pending_invoice_item_invoice": null,
"pause_collection": null,
"pending_invoice_item_interval": null,
"pending_setup_intent": null,
"pending_update": null,
"schedule": null,
"start_date": 1624221102,
"status": "active",
"transfer_data": null,
"trial_end": null,
"trial_start": null
}
When the "customer.subscription.created" I want to update the price id ("price_1J3q2zHx6drz9dmy2bMdkXOs") but I am not sure how to retrieve it. I have tried:
session.items.data[price.id]
but it doesn't work. How do I access the price id?
Related
i'm using the node.js client library of google transcoder api. I'm able to retrive a paginated list of some jobs, but i'm not able to order elements by start date. Here my codes:
const { TranscoderServiceClient } = require('#google-cloud/video-transcoder').v1;
class TranscoderApiController {
constructor() {
this.projectId = process.env.GOOGLE_CLOUD_PROJECT;
this.location = process.env.TASK_LOCATION;
}
async getEntries(req, res, next) {
const params = {
pageSize: req.query.pageSize ? parseInt(req.query.pageSize) : 10,
pageToken: req.query.pageToken,
filter: req.query.filter,
orderBy: req.query.orderBy
}
const client = new TranscoderServiceClient();
const result = await client.listJobs({
parent: client.locationPath(this.projectId, this.location),
pageSize: params.pageSize,
orderBy: 'createTime.seconds'
}, {
autoPaginate: false
});
if (result.length == 3 && result[2] != undefined) {
return result[2];
} else {
return result[1];
}
}
}
module.exports = new TranscoderApiController();
When i call the getEntries method i receive the following error:
"3 INVALID_ARGUMENT: The request was invalid: sort order \"createTime.seconds\" is unsupported"
If i remove the orderBy: 'createTime.seconds' line then the api works but is not ordered as i want. The result is something like that (i abbreviate the json):
{
"jobs": [
{
"labels": {},
"name": "projects/<id>/locations/europe-west1/jobs/<uuid>",
"inputUri": "",
"outputUri": "",
"state": "SUCCEEDED",
"createTime": {
"seconds": "1656602896",
"nanos": 386772728
},
"startTime": {
"seconds": "1656602900",
"nanos": 755000000
},
"endTime": {
"seconds": "1656603062",
"nanos": 428000000
},
"ttlAfterCompletionDays": 30,
"error": null,
"config": {
"inputs": [
{
"key": "input0",
"uri": "gs://<url>/render_md.mp4",
"preprocessingConfig": null
}
],
"editList": [...],
"elementaryStreams": [...],
"muxStreams": [...],
"manifests": [],
"adBreaks": [],
"spriteSheets": [],
"overlays": [],
"output": {
"uri": "gs://<url>/md.mp4/"
},
"pubsubDestination": {
"topic": "projects/<id>/topics/transcoder_api"
}
},
"jobConfig": "config"
},
...
],
"unreachable": [],
"nextPageToken": "Co8BCjgKDGV1cm9wZS13ZXN0MRIZdHJhbnNjb2Rlci5nb29nbGVhcGlzLmNvbRgBII..."
}
As you can see each job have the startTime.seconds property. I follow the syntax described here:
https://google.aip.dev/132#ordering
Any support to solve the ordered issue is appreciated.
const functions = require("firebase-functions");
const Razorpay = require("razorpay");
const cors = require("cors")({origin: true});
exports.razorpayverification = functions.https.onRequest((req, res) => {
try {
const name = JSON.stringify(req.body);
console.log(req.body, name);
res.status(200).json({status: "ok"});
// I need req.body.amount value. req.body is empty. but JSON.stringify(req.body) contains all the data.
} catch (error) {
res.status(400).json({error: "JSON error"});
res.end();
}
});
I am using the above cloud function and receiving payment data from Razorpay webhooks. In my console.log I have called req.body and name (const name = JSON.stringify(req.body);). name returns the data in string format and contains all information like orderid, payment id, amount etc but req.body is empty or undefined. I tried enclosing the above within cors but still the same result. Kindly let me know how to read req.body. I need to read req.body.amount.
Output:
req.body:
{
name: {"entity":"event","account_id":"acc_IvsP3zm1EoP7me","event":"payment.captured","contains":["payment"],"payload":{"payment":{"entity":{"id":"pay_J9fXFPd7T9uWo5","entity":"payment","amount":1000,"currency":"INR","status":"captured","order_id":"order_J9fWySTzmrenBl","invoice_id":null,"international":false,"method":"card","amount_refunded":0,"refund_status":null,"captured":true,"description":"Recharge","card_id":"card_J9fXFSITSAyVdB","card":{"id":"card_J9fXFSITSAyVdB","entity":"card","name":"k","last4":"1111","network":"Visa","type":"debit","issuer":null,"international":false,"emi":false,"sub_type":"consumer","token_iin":null},"bank":null,"wallet":null,"vpa":null,"email":"test#test.com","contact":"+16505551234","notes":{"key1":"otzo","key2":"recharge","address":"Razorpay Corporate Office"},"fee":20,"tax":0,"error_code":null,"error_description":null,"error_source":null,"error_step":null,"error_reason":null,"acquirer_data":{"auth_code":"213319"},"created_at":1647850380}}},"created_at":1647850385}
As shown req.body returns only the open curly braces and nothing after that. name returns all the data.
Not sure this is the correct answer but my code was executing properly and I was receiving the res.body. But issue lies in the console.log which did not display. if it is a multi-level object firebase cloud function is not displaying properly. Below is the Razorpays Payments Sample Payload. res.body displays only open end curly bracket. res.body.payload created log with all the details under the payload section. Again here firebase cloud function displays each and every field as a separate log.
{ "entity": "event", "account_id": "acc_BFQ7uQEaa7j2z7", "event": "payment.authorized", "contains": [ "payment" ], "payload": { "payment": { "entity": { "id": "pay_DESlfW9H8K9uqM", "entity": "payment", "amount": 100, "currency": "INR", "status": "authorized", "order_id": "order_DESlLckIVRkHWj", "invoice_id": null, "international": false, "method": "netbanking", "amount_refunded": 0, "refund_status": null, "captured": false, "description": null, "card_id": null, "bank": "HDFC", "wallet": null, "vpa": null, "email": "gaurav.kumar#example.com", "contact": "+919876543210", "notes": [], "fee": null, "tax": null, "error_code": null, "error_description": null, "error_source": null, "error_step": null, "error_reason": null, "acquirer_data": { "bank_transaction_id": "0125836177" }, "created_at": 1567674599 } } }, "created_at": 1567674606}
I'm trying to update the values of my payments array objects
{
"balance": 109610,
"gifts": [],
"orders": [],
"payments": [{
"isPaid": 0,
"status": "Pending",
"address": "3KsdQbmADyz1KNN7qqX1yZcMXBbfFCm31r",
"date": 1624057559970
}, {
"isPaid": 0,
"status": "Pending",
"address": "3FYQK6YiAaL8fEbDWaXYw38CJN3K2y5dPD",
"date": 1624058531601
}],
"isVendedor": false,
"isAdmin": true,
"createdAt": {
"$date": "2021-06-17T21:10:15.020Z"
},
"username": "teste",
"email": "teste#teste.com",
"password": "$2a$10$qUNkorDuvbf.AYLTvjNc4ebKyNgLa7L9NoTBwAIV8.BfN51umaD9O",
"__v": 3
}
First, I look for the object of the user who made a request to my server
const userPayment = await User.find({"payments.address": notification.address}).exec();
Then I go through the user object and find it until I find the position where I find notification.address again
userPayment.forEach((AllPayments, index) => {
AllPayments.payments.forEach((payment, index) => {
if (payment.address == notification.address) {
if (payment.isPaid || payment.status != "Pending")
return res.json({
success: false,
error: "Payment Already Processed!",
});
const valueToDeposit = Math.round(notification.fiat_amount);
console.log(
userPayment[0].payments[index].isPaid,
userPayment[0].payments[index].status
);
// Set payments[index].isPaid = true
// Set payments[index].status = "Paid"
});
});
So I tried to make these 3 ways and none of them was successful.
userPayment[0].balance += valueToDeposit; // this works when save() is triggered
userPayment[0].payments[index].isPaid = 1; // this doesnt works when save() is triggered
userPayment[0].payments[index].status = "Paid"; // this doesnt works when save() is triggered
userPayment[0].updateOne({"payments.address": notification.address}, { $set: { "payments.$.isPaid": 1,"payments.$.status":"Paid" } },(err, result) => { console.log(err, result); }); this doesnt works
userPayment[0].save()
When I do the following:
const queryChannelsResponse = await this.client.queryChannels(
{ id: channelId },
{ last_updated_at: -1 },
{ state: true },
This does not include the members. How am I able to get the member information?
I am writing a webhook and I want it to send a push notification (I am currently sending these myself via expo) to all offline users.
I am migrating from pusher chatkit which is now being discontinued. They had a new_message_users_offline hook for this purpose.
In the message.new webhook payload in the documentation the members are present but they are not present in the request body:
{
"type": "message.new",
"cid": "messaging:394f36fd-d512-4f2b-a785-ab8dfe82af49",
"message": {
"id": "f73ee1a8-f6fd-450b-bc64-0840b4df8fd9-2b4908ad-e267-4c48-8f41-8c26c8f769ce",
"text": "Ffddf",
"html": "<p>Ffddf</p>\n",
"type": "regular",
"user": {
"id": "f73ee1a8-f6fd-450b-bc64-0840b4df8fd9",
"role": "user",
"created_at": "2020-04-06T14:06:37.979584Z",
"updated_at": "2020-04-06T19:45:39.556842Z",
"last_active": "2020-04-06T19:45:39.54939Z",
"banned": false,
"online": true,
"name": "Mark Everett",
"image": "https://8dc-user-files-dev.s3.eu-west-1.amazonaws.com/MEMBER_PROFILE_IMAGE-f73ee1a8-f6fd-450b-bc64-0840b4df8fd9.png?v=6"
},
"attachments": [],
"latest_reactions": [],
"own_reactions": [],
"reaction_counts": null,
"reaction_scores": {},
"reply_count": 0,
"created_at": "2020-04-06T19:51:14.114803Z",
"updated_at": "2020-04-06T19:51:14.114803Z",
"mentioned_users": []
},
"user": {
"id": "f73ee1a8-f6fd-450b-bc64-0840b4df8fd9",
"role": "user",
"created_at": "2020-04-06T14:06:37.979584Z",
"updated_at": "2020-04-06T19:45:39.556842Z",
"last_active": "2020-04-06T19:45:39.54939Z",
"banned": false,
"online": true,
"channel_unread_count": 0,
"channel_last_read_at": "1970-01-01T00:00:00Z",
"total_unread_count": 0,
"unread_channels": 0,
"unread_count": 0,
"image": "https://8dc-user-files-dev.s3.eu-west-1.amazonaws.com/MEMBER_PROFILE_IMAGE-f73ee1a8-f6fd-450b-bc64-0840b4df8fd9.png?v=6",
"name": "Mark Everett"
},
"watcher_count": 1,
"created_at": "2020-04-06T19:51:14.121213459Z",
"channel_type": "messaging",
"channel_id": "394f36fd-d512-4f2b-a785-ab8dfe82af49"
}
My plan is do do something like this:
public async getOfflineUserIds(channelId: string): Promise<string[]> {
try {
// Get the channel
const queryChannelsResponse = await this.client.queryChannels(
{ id: channelId },
{ last_updated_at: -1 },
{ message_limit: 0, limit: 1, state: true},
)
const channel = queryChannelsResponse[0]
console.log('channel: ', channel)
// Get the channels members
const userIds: string[] = []
// tslint:disable-next-line: forin
for (const index in channel.state.members) {
userIds.push(channel.state.members[index].user_id)
}
console.log('userIds:', userIds)
const queryUsersResponse = await this.client.queryUsers(
{ id: { $in: userIds } },
{ last_active: -1 },
{},
)
console.log('queryUsersResponse:', queryUsersResponse)
// Work out who is offline/online
const offlineUserIds = queryUsersResponse.users
.filter(u => !u.online)
.map(u => u.id)
return offlineUserIds
} catch (err) {
throw new InternalServerErrorException(
'Error getting offline users for channel.',
err,
)
}
}
This is now resolved.
I did not add the members to the channel with channel.addMembers. I create and add members on the server as this works perfectly for my use case.
If it helps anyone I ended up with these two methods:
public async getChannelUserIds(channelId: string): Promise<string[]> {
try {
const queryChannelsResponse = await this.client.queryChannels(
{ id: channelId },
{ last_updated_at: -1 },
{ message_limit: 0, limit: 1, state: true },
)
const channel = queryChannelsResponse[0]
const userIds = Object.keys(channel.state.members)
console.log('userIds:', userIds)
return userIds
} catch (err) {
throw new InternalServerErrorException(
`Error getting user ids for channel ('${channelId}').`,
err,
)
}
}
public async getOfflineUserIds(userIds: string[]): Promise<string[]> {
try {
const queryUsersResponse = await this.client.queryUsers(
{ id: { $in: userIds } },
{ last_active: -1 },
{},
)
console.log('queryUsersResponse:', queryUsersResponse)
const offlineUserIds = queryUsersResponse.users
.filter(u => !u.online)
.map(u => u.user_id)
return offlineUserIds
} catch (err) {
throw new InternalServerErrorException(
`Error getting offline user ids from ('${JSON.stringify(
userIds,
null,
2,
)}').`,
err,
)
}
}
And then in my webhook I:
#Post('stream/messages')
public async onReceive(
#Req() req: Request,
#Headers('x-signature') signature: string,
#Body() body: any,
) {
try {
console.debug('webhooks-stream.messages.onReceive')
this.chatService.verifyWebhook((req as any).rawBody, signature)
console.log('DEBUG WEBHOOK BODY', JSON.stringify(body, null, 2))
switch (body.type) {
case 'message.new': {
const offlineMemberIds = await this.chatService.getOfflineUserIds(
body.members.map(member => member.user_id),
)
...
I am trying to pass a UID and purchase ID with Stripe Checkout session object (using metadata). Generating the session ID on my server attaching the metadata works very fine. Stripe also POSTs everything correctly to my webhook server. The problems occurs while retrieving the metadata from the session object POSTed by Stripe.
Here is the error I get
TypeError: Cannot read property 'metadata' of undefined at /app/app.js:35:32
Here is the session obj posted by Stripe-
{
"id": "evt_1GRC7lAfcfWZXl7jQ3VzNo4y",
"object": "event",
"api_version": "2019-10-17",
"created": 1585292221,
"data": {
"object": {
"id": "cs_test_gLsHqtF8XhB3C3DlWKcLtNdTitp0St8ju5qgJgl6tHrMxxWvju9gb9Li",
"object": "checkout.session",
"billing_address_collection": null,
"cancel_url": "https://andropaym.firebaseapp.com/fail.html",
"client_reference_id": null,
"customer": "cus_GzASi1Klpydh8x",
"customer_email": null,
"display_items": [
{
"amount": 37500,
"currency": "inr",
"custom": {
"description": "Carefully modified Linux Distro Bundle for Android.",
"images": null,
"name": "Modded OS Bundle"
},
"quantity": 1,
"type": "custom"
}
],
"livemode": false,
"locale": null,
"metadata": {
"uid": "EB1m6nAOTVNcQhHO2O7COspap8y1",
"payID": "GPA.5620-9852-7063-44324"
},
"mode": "payment",
"payment_intent": "pi_1GRC7EAfcfWZXl7jhixrWHRS",
"payment_method_types": [
"card"
],
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "https://andropaym.firebaseapp.com/success.html"
}
},
"livemode": false,
"pending_webhooks": 4,
"request": {
"id": null,
"idempotency_key": null
},
"type": "checkout.session.completed"
}
Here is my webhook code -
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const stripe = require('stripe')('sk_test_xxxx');
const endpointSecret = 'whsec_xxxx';
// set the port of our application
// process.env.PORT lets the port be set by Heroku
var port = process.env.PORT || 8080;
app.post('/', bodyParser.raw({type: 'application/json'}), (request, response) => {
const sig = request.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
} catch (err) {
return response.status(400).send(`Webhook Error: ${err.message}`);
}
function handleCheckoutSession(uid) {
// Here we are getting the session obj and we can process it to check for the things we need
console.log("UID is " + uid);
}
// Handle the checkout.session.completed event
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
let uid = request.data.metadata.uid;
// Fulfill the purchase...
handleCheckoutSession(uid);
}
// Return a response to acknowledge receipt of the event
response.json({received: true});
});
app.listen(port, function () {
console.log('Our app is running on http://localhost:' + port);
});
module.exports = app;
The code works fine without the metadata being parsed
More code links:
1. Highlighted error webhook code - https://gist.github.com/imprakharshukla/1e2315615983e0e9d492d2288e159832#file-webhook_backend-js-L40
You need to use the object returned by stripe.constructEvent, not the request body.
Change
let uid = request.data.metadata.uid;
to
let uid = session.metadata.uid
and it should work as expected.