Skip to content

Instantly share code, notes, and snippets.

@azivkovi
Last active March 15, 2024 10:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save azivkovi/7ca41aee6a21db1616b917d57fa2757c to your computer and use it in GitHub Desktop.
Save azivkovi/7ca41aee6a21db1616b917d57fa2757c to your computer and use it in GitHub Desktop.
webhooks.js
import Stripe from 'stripe';
import dayjs from 'dayjs';
import prisma from '@/lib/prisma.lib';
import { evaluateRegistration } from '../evaluateRegistration';
import { saveInvoice } from '@/helpers/save-invoice';
import { sendInvoice } from '@/helpers/send-invoice';
import { createFiscalizedInvoice } from '@/helpers/create-fiscalized-invoice';
import { mailTransport } from '@/lib/nodemailer.lib';
export const config = {
api: {
bodyParser: false,
},
};
async function buffer(readable) {
const chunks = [];
for await (const chunk of readable) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
}
return Buffer.concat(chunks);
}
// @desc Interact with Stripe subscription webhooks
// @route POST /api/v1/stripe/webhooks
// @access Private
const handler = async (req, res) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
if (req.method === 'POST') {
const buf = await buffer(req);
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
if (!sig || !webhookSecret) return;
const event = stripe.webhooks.constructEvent(buf, sig, webhookSecret);
const { type: eventType } = event;
if (eventType === 'payment_intent.created') {
}
if (eventType === 'payment_intent.succeeded') {
}
if (eventType === 'checkout.session.completed') {
const {
data: {
object: {
id: checkoutId,
subscription: subscriptionId,
metadata: { installments, connect },
amount_total: amountTotal,
payment_intent: paymentIntentId,
},
},
} = event;
const registration = await prisma.registrations.findFirst({
where: {
checkoutId,
},
});
const {
id,
email,
firstName,
lastName,
education,
company,
slug,
educationType,
level,
validFor,
} = registration;
const price = new Intl.NumberFormat('hr', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amountTotal / 100);
// Napravi fiskalizirani racun na Solo
const invoice = await createFiscalizedInvoice({
education,
company,
price,
customer: `${firstName} ${lastName}`,
});
if (invoice.status !== 0) {
console.log('Invoice error', invoice);
//TODO: Prikazi gresku i zatrazi refund od Stripe?
}
// Sacuvaj racun u bazu
await saveInvoice({
invoiceNumber: invoice?.racun?.broj_racuna,
email,
customer: `${firstName} ${lastName}`,
education,
fileUrl: invoice?.racun?.pdf,
});
// Ako nije jednokratna uplata napravi subscription schedule
if (subscriptionId) {
let schedule = await stripe.subscriptionSchedules.create({
from_subscription: subscriptionId,
});
// Ako ima odredjeni broj uplata (pretplata ima 0), dodaj faze sa zeljenim brojem iteracija
if (parseInt(installments) > 0) {
const phases = schedule.phases.map((phase) => ({
start_date: phase.start_date,
end_date: phase.end_date,
items: phase.items,
}));
schedule = await stripe.subscriptionSchedules.update(schedule.id, {
end_behavior: 'cancel',
phases: [
...phases,
{
items: phases[0].items,
iterations: parseInt(installments),
},
],
});
}
await prisma.registrations.update({
where: {
checkoutId,
},
data: {
subscriptionId,
},
});
}
let relevantValidFromDate = dayjs();
if (educationType !== 'subscription') {
// Dobavi podatke o edukaciji
const educationRes = await fetch(
`${process.env.WP_LOCAL_URL}/wp-json/wp/v2/posts?slug=${slug}`
);
const educationData = await educationRes.json();
relevantValidFromDate =
educationData[0].acf['1_datum'] &&
educationData[0].acf['1_datum'] !== '' &&
dayjs().isBefore(educationData[0].acf['1_datum'])
? dayjs(educationData[0].acf['1_datum'])
: dayjs();
}
// Odobri prijavu s checkoutId
await evaluateRegistration({
id,
slug,
email,
firstName,
lastName,
education,
educationType,
status: 'approved',
reviewedBy: 'App',
level,
validFor,
validFrom: relevantValidFromDate.toDate(),
invoiceUrl: invoice?.racun?.pdf || '--',
});
// Posalji admin mail
const adminMailParams = {
from: `Izvrsnost.hr - Prijava <${process.env.EMAIL_ADDRESS}>`,
to: process.env.EMAIL_ADDRESS,
replyTo: email,
subject: `Prijava za - ${education} ${level ? `- ${level}` : ''}`,
html: `
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Ime</strong></td>
<td>${firstName}</td>
</tr>
<tr>
<td><strong>Prezime</strong></td>
<td>${lastName}</td>
</tr>
<tr>
<td><strong>Email</strong></td>
<td>${email}</td>
</tr>
<tr>
<td><strong>Način plaćanja</strong></td>
<td>Stripe</td>
</tr>
</tbody>
</table>
`,
};
if (connect === 'true' && parseInt(installments) === 0) {
const stripeConnect = await prisma.stripe_connect.findFirst({
where: {
slug,
},
});
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
await stripe.transfers.create({
amount: Math.floor((paymentIntent.amount * stripeConnect.percent_share) / 100),
currency: paymentIntent.currency,
destination: stripeConnect.accountId,
transfer_group: paymentIntentId,
});
}
// await mailTransport.sendMail(adminMailParams);
}
if (eventType === 'invoice.payment_succeeded') {
const {
data: {
object: { billing_reason, subscription: subscriptionId, amount_due: amountDue },
},
} = event;
if (billing_reason === 'subscription_create') {
// Pokriveno u checkout.session.completed
}
if (billing_reason === 'subscription_cycle') {
const registration = await prisma.registrations.findFirst({
where: {
subscriptionId,
},
});
const { email, firstName, lastName, education, company } = registration;
const price = new Intl.NumberFormat('hr', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amountDue / 100);
// Napravi fiskalizirani racun na Solo
const invoice = await createFiscalizedInvoice({
education,
company,
price,
customer: `${firstName} ${lastName}`,
});
if (invoice.status !== 0) {
console.log('Invoice error', invoice);
//TODO: Prikazi gresku i zatrazi refund od Stripe?
}
// Posalji racun na mail
await sendInvoice({
email,
firstName,
lastName,
education,
invoiceUrl: invoice?.racun?.pdf || '--',
});
// Sacuvaj racun u bazu
await saveInvoice({
invoiceNumber: invoice?.racun?.broj_racuna,
email,
customer: `${firstName} ${lastName}`,
education,
fileUrl: invoice?.racun?.pdf,
});
}
}
if (eventType === 'payment_intent.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.
//TODO: Posalji mail korisniku da naplata nije uspijela. Ukljuci i customer portal link. Napisi kada ce biti sljedeci pokusaj naplate
}
if (eventType === 'checkout.session.expired') {
const {
data: {
object: { id: checkoutId },
},
} = event;
await prisma.registrations.update({
where: {
checkoutId,
},
data: {
status: 'expired',
},
});
}
if (eventType === 'customer.subscription.deleted') {
const {
data: {
object: { id: subscriptionId, current_period_end },
},
} = event;
await prisma.registrations.update({
where: {
subscriptionId,
},
data: {
expiresAt: dayjs(new Date(current_period_end * 1000)).toDate(),
},
});
}
} catch (error) {
console.log(`Webhook error: ${error.message}`);
return res.status(400).send(`Webhook error: ${error.message}`);
}
}
res.status(200).send();
};
export default handler;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment