skip to Main Content

Strapi send email after payment compelete

I have a react frontend with a strapi backend.

I’m adding products to a cart and then submitting that cart to Stripe

This is all working.

When the payment is completed I would like to send an email.

I have a sendgrid account.

I have added sendgrid to strapi with

yarn add @strapi/provider-email-sendgrid

I have added my sendgrid app key to the .env file

In config/plugins I have

module.exports = ({ env }) => ({
    email: {
        config: {
            provider: 'sendgrid',
            providerOptions: {
                apiKey: env('SENDGRID_API_KEY'),
            },
            settings: {
                defaultFrom: '[email protected]',
                defaultReplyTo: '[email protected]',
            },
        },
    },
});

I have a orders content type

In src/api/order/controllers/order.ts I have

"use strict";
// @ts-ignore
const stripe = require("stripe")("sk_test_51H");

/**
 *  order controller
 */
const { createCoreController } = require("@strapi/strapi").factories;
module.exports = createCoreController("api::order.order");

module.exports = {
    setUpStripe: async (ctx) => {

    let total = 0
    let validatedCart = []
    let receiptCart = []

    const { cart } = ctx.request.body

    await Promise.all(cart.map(async product => {
        try {

            const validatedProduct = await strapi.db.query('api::product.product').findOne({
                where: { id: product.id }
            });

            if (validatedProduct) {
                validatedCart.push(validatedProduct);

                receiptCart.push({
                    id: product.id
                })
            }
        } catch (error) {
            console.error('Error while querying the databases:', error);
        }
    }));

    total = validatedCart.reduce((n, { price }) => n + price, 0) || 0;

    try {
        const paymentIntent = await stripe.paymentIntents.create({
            amount: total,
            currency: 'usd',
            metadata: { cart: JSON.stringify(validatedCart.toString()) },
            payment_method_types: ['card']
        });

        // Send a response back to the client
        ctx.send({
            message: 'Payment intent created successfully',
            paymentIntent,
        });
    } catch (error) {
        // Handle errors and send an appropriate response
        ctx.send({
            error: true,
            message: 'Error in processing payment',
            details: error.message,
        });
    }
},

};

in src > api > order > content-types > order > lifecycles.js

module.exports = {
    lifecycles: {
        async afterCreate(event) {
            const { result } = event

            try {
                await strapi.plugins['email'].services.email.send({

                    to: '[email protected]',
                    from: '[email protected]',
                    subject: 'Thank you for your order',
                    text: `Thank you for your order ${result.name}`

                })
            } catch (err) {
                console.log(err)
            }
        }
    }
}

in the strapi admin > settings > configuration

the send test email works

The email sent lifecycles.js doesn’t work.

Does anyone have any ideas why this might not work or how I can debug this

2

Answers


  1. I’m not sure what version of strapi you are using, or if you are using TypeScript to compile to JavaScript.

    You might want to try using vanilla JS until you get a better handle on things. If you want to use TS then use import/export syntax and not require/exports.

    Using vanilla JS I would suggest these changes:

    In src/api/order/controllers/order.js <— Note the change from .ts to .js

    const { createCoreController } = require("@strapi/strapi").factories;
    // Only define module.exports once
    module.exports = createCoreController("api::order.order", {
        // Your previous implementation
        
        setUpStripe: async (ctx) => {
    
        let total = 0
        let validatedCart = []
        let receiptCart = []
    
        const { cart } = ctx.request.body
    
        await Promise.all(cart.map(async product => {
            try {
    
                const validatedProduct = await strapi.db.query('api::product.product').findOne({
                    where: { id: product.id }
                });
    
                if (validatedProduct) {
                    validatedCart.push(validatedProduct);
    
                    receiptCart.push({
                        id: product.id
                    })
                }
            } catch (error) {
                console.error('Error while querying the databases:', error);
            }
        }));
    
        total = validatedCart.reduce((n, { price }) => n + price, 0) || 0;
    
        try {
            const paymentIntent = await stripe.paymentIntents.create({
                amount: total,
                currency: 'usd',
                metadata: { cart: JSON.stringify(validatedCart.toString()) },
                payment_method_types: ['card']
            });
    
            // Send a response back to the client
            ctx.send({
                message: 'Payment intent created successfully',
                paymentIntent,
            });
        } catch (error) {
            // Handle errors and send an appropriate response
            ctx.send({
                error: true,
                message: 'Error in processing payment',
                details: error.message,
            });
        }
    });
    

    In src/api/order/content-types/order/lifecycles.js:

    module.exports = {
        // Do not nest under "lifecycles" key
        async afterCreate(event) {
            const { result } = event
    
            try {
                await strapi.plugins['email'].services.email.send({
    
                    to: '[email protected]',
                    from: '[email protected]',
                    subject: 'Thank you for your order',
                    text: `Thank you for your order ${result.name}`
    
                })
            } catch (err) {
                console.log(err)
            }
        }
    }
    
    Login or Signup to reply.
  2. Stripe uses webhooks to tell to your backend if a payments was successful.

    You can use it as a trigger to send an email to the concerned user. You can either customize the email according to the user’s order.

    You first have to add a String field to the user to store his Stripe’s Customer ID. You can do it through the admin panel with the Content Manager.

    When the user create his account, You have to create his Stripe Customer profile from Backend.

    I am using JS instead of TS, but you will be able to do the same thing normally

    For easier development, I set my Stripe object in the Strapi object at bootstrap: (Not mandatory, but easier for me as I don’t need to re-instanciate Stripe each time I need it)

    // src/index.js
    
    "use strict";
    
    module.exports = {
    ...
        async bootstrap({ strapi }) {
    
            // Start Stripe and add it to Strapi instance
            strapi.stripe = require('stripe')(process.env.STRIPE_SECRET);
    
        }
    ...
    }
    

    For exemple, I have a mandatory step where my users sends me all their information like their first name, last name and profile picture. I use this route to create the Stripe Customer

    // My profile creation controller
    
    module.exports = {
        ...
        
        createProfile = async (ctx, next) => {
            const user = ctx.state.user;
    
            try {
                ...
    
                // Create a stripe customer for the user
                const stripeCustomer = await strapi.stripe.customers.create({
                    email: user.email,
                    name: `${firstName} ${lastName}`, // Not mandatory, but useful if you have access to this data
                });
    
                if (!stripeCustomer) {
                    return ctx.internalServerError(
                        'Could not create stripe customer'
                    );
                }
    
                // Update the user with the stripeCustomerId
                let updatedUser = await strapi.entityService
                    .update('plugin::users-permissions.user', user.id, {
                        data: {
                            stripeCustomerId: stripeCustomer.id // My user's customer ID is stored in a field that I called stripeCustomerId
                        }
                    });
            } catch(e) {
                ...
            }
            ...
        }
    }
    

    Each time an order will be done, you have to create a PaymentIntent for Stripe.

    You have to store the PaymentIntent ID somewhere. A way I recommand you to do this is, for example if you store all your orders on the database, is to attach an order to the PaymentIntent ID with a new private String field.

    You can also add a status enumeration to follow the progression of the order.

    But the way I am doing this is that whatever the order is paid or not, I create an Order element in my db, containing all the data needed as if it was paid and have to be processed, but the status is in waitingPayment state.

    So I can attach the PaymentIntent ID to the order, even if the order is not confirmed.

    To prevent useless data storing, I have a CRON job that check every hour if their is some orders that are not confirmed (order.status === 'waitingPayment'). If it is the case, and the order have been created more than an hour before, I can delete it.

    I also added a way that when a user is trying to create a new order, I check if it has not already created one that was not finished.

    // Order Controller file
    
    'use strict';
    
    module.exports = {
    
        async create(ctx, next) {
            const user = ctx.state.user;
    
            // Check if a previous unpaid order was created by the user
            const previousOrders = await strapi.entityService.findMany(
                'api::order.order',
                {
                    filters: {
                        user: user.id,
                        status: 'waitingPayment',
                    }
                }
            );
    
            // Cancel the previous unpaid orders
            if (previousOrders.length > 0) {
                for (const order of previousOrders) {
                    try {
                        // Cancel Stripe paymentIntent
                        await strapi.stripe.paymentIntents.cancel(
                            order.paymentIntentId
                        );
    
                        await strapi.entityService.delete(
                            'api::order.order',
                            order.id
                        );
                    } catch (error) {
                        console.error(error);
                    }
                }
            }
    
            // We need to create the object before assign the paymentIntent to it to be able to add useful metadata to the Stripe's PaymentIntent
            let order = null;
            try {
                order = await strapi.entityService.create(
                    'api::order.order',
                    {
                        data: {
                            ...
                            status: 'waitingPayment',
                            createdAt: new Date(),
                            publishedAt: new Date(),
                        },
                    }
                );
            } catch (error) {
                console.error(error.details);
                return ctx.internalServerError(error);
            }
    
            const ephemeralKey = await strapi.stripe.ephemeralKeys.create(
                { customer: user.stripeCustomerId },
                { apiVersion: '2023-10-16' }
            );
    
            // Create the PaymentIntent. you can customize it as needed
            const paymentIntent = await strapi.stripe.paymentIntents.create({
                amount: cost.total,
                currency: user.currency,
                payment_method_types: ['card'],
                customer: user.stripeCustomerId, // The only useful thing
                metadata: {
                    order: order.id, // Used to identify the announce on webhook
                },
            });
    
            if (!paymentIntent) {
                return ctx.internalServerError('Could not create payment intent');
            }
    
            if (!order) {
                await strapi.stripe.paymentIntents.cancel(paymentIntent.id);
                return ctx.internalServerError('Could not create order');
            }
    
            try {
                // Add paymentIntent ID to the order element
                order = await strapi.entityService.update(
                    'api::order.order',
                    order.id,
                    {
                        data: {
                            user: user.id, // Attach current user to the order to be able to retreive its information from the order (Relation field)
                            paymentIntentId: paymentIntent.id,
                        },
                    }
                );
            } catch (error) {
                console.error(error.details);
                return ctx.internalServerError(error);
            }
            ...
        },
        ...
    };
    

    This controller is used to send to the front-end all the needed information to create the Stripe Payment. But it is not the topic.

    Now that we have stored the PaymentIntent ID, and that we have attached the order id to the Stripe’s metadata, we just have to wait the webhooks to give us a response.

    So, create a public API reserved for Stripe’s webhooks.

    I created for example the /api/payment POST route.

    // src/api/payment/routes/payment.js
    
    'use strict';
    
    module.exports = {
        routes: [
            {
                method: 'POST',
                path: '/payment',
                handler: 'payment.webhook', // Refers to the controller's file
                config: {
                    policies: [],
                    middlewares: [],
                    auth: false, // Set it as public
                },
            },
        ],
    };
    

    Now, in the controller, we will finally be able to send our email once we receive the good webhook call.

    // src/api/payment/controllers.payment.js
    
    'use strict';
    
    /**
     * A set of functions called "actions" for `payment`
     */
    
    // Stripe check if the request is from Stripe, it checks both the content and the format of the request.
    // Strapi will automatically parse the body of the request and store it in ctx.request.body, but it will apply some changes in the format of the request, so Stripe will not be able to verify the request.
    // To solve this issue, we need to use the unparsedBody symbol to access the raw body of the request without any changes.
    const unparsed = Symbol.for('unparsedBody');
    
    module.exports = ({ strapi }) => ({
        webhook: async (ctx, next) => {
            let event = ctx.request.body[unparsed];
    
            const endpointSecret = process.env.STRIPE_ENDPOINT_SECRET;
    
            // Only verify the event if you have an endpoint secret defined.
            // Otherwise, use the basic event deserialized with JSON.parse
            if (endpointSecret) {
                // Get the signature sent by Stripe
                const signature = ctx.request.headers['stripe-signature'];
                try {
                    event = strapi.stripe.webhooks.constructEvent(
                        // Give ctx.request.body as Buffer to constructEvent
                        ctx.request.body[unparsed],
                        signature,
                        endpointSecret
                    );
                } catch (err) {
                    console.log(
                        `⚠️  Webhook signature verification failed.`,
                        err.message
                    );
                    return ctx.send({ error: 'Webhook Error' }, 400);
                }
            }
    
            // Handle the event
            switch (event.type) {
                case 'payment_intent.succeeded':
                    // Here you can trigger that a payment has succeeded !
                    // I created a service to handle each event received, but the only one we need in this case is this one
                    await strapi
                        .service('api::payment.payment')
                        .handlePaymentIntentSucceeded(event.data.object); // All the data we need is stored in this `event.data.object`
                    break;
                ...
            }
    ...
    

    last thing to do, now that we can trigger when a payment succeeded, is to handle it accordingly. The previous example I wrote you use a service. We can do that so:

    // src/api/payment/services/payment.js
    
    'use strict';
    
    /**
     * payment service
     */
    
    module.exports = ({ strapi }) => ({
        handlePaymentIntentSucceeded: async (paymentIntent) => {
            console.log(
                'PaymentIntent with id: ' + paymentIntent.id + ' succeeded'
            );
            // Find the order, and update its status
            const order = await strapi.entityService.update(
                'plugin::carcheker.announce',
                paymentIntent.metadata.order, // We can get the order id back from the paymentIntent metadata
                {
                    data: {
                        status: 'paid', // Change its status by whatever you want since the value is set on the enumeration of the `status` field
                    },
                    populate: {
                        user: true, // We will be able to recover user email
                    },
                }
            );
    
            // Now you have access to the user email from `order.user.email`
            const userEmail = order.user.email;
    
            // TODO: Send your email
        },
    

    I am using the email-designer Strapi’s plugin, that is very useful to create and manage email templates, I recommand it.

    With that, you just have to send your email with the user email.

    I give you my example with the email-designer plugin:

    await strapi
                .plugin('email-designer')
                .service('email')
                .sendTemplatedEmail(
                    {
                        to: order.user.email,
                        from: process.env.EMAIL_ADDRESS_FROM,
                        replyTo: process.env.EMAIL_ADDRESS_REPLY,
                        attachments: [],
                    },
                    {
                        // required - Ref ID defined in the template designer (won't change on import)
                        templateReferenceId: 200,
    
                        // If provided here will override the template's subject.
                        // Can include variables like `Thank you for your order {{= USER.firstName }}!`
                        subject: `Your order #${order.id} has been registered!`,
                    },
                    {
                        // Variables to use in the template
                    }
                );
    

    See the webhook setup guide from Stripe in Node mode for further details:
    https://docs.stripe.com/webhooks/quickstart

    Hope this will help you !

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search