import Stripe from 'stripe'; import { Injectable } from '@nestjs/common'; import { Organization, User } from '@prisma/client'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; import { BillingSubscribeDto } from '@gitroom/nestjs-libraries/dtos/billing/billing.subscribe.dto'; import { capitalize, groupBy } from 'lodash'; import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service'; import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; import { AuthService } from '@gitroom/helpers/auth/auth.service'; import { TrackService } from '@gitroom/nestjs-libraries/track/track.service'; import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-04-10', }); @Injectable() export class StripeService { constructor( private _subscriptionService: SubscriptionService, private _organizationService: OrganizationService, private _userService: UsersService, private _trackService: TrackService ) {} validateRequest(rawBody: Buffer, signature: string, endpointSecret: string) { return stripe.webhooks.constructEvent(rawBody, signature, endpointSecret); } async updateAccount(event: Stripe.AccountUpdatedEvent) { if (!event.account) { return; } const accountCharges = event.data.object.payouts_enabled && event.data.object.charges_enabled && !event?.data?.object?.requirements?.disabled_reason; await this._subscriptionService.updateConnectedStatus( event.account!, accountCharges ); } async checkValidCard( event: | Stripe.CustomerSubscriptionCreatedEvent | Stripe.CustomerSubscriptionUpdatedEvent ) { if (event.data.object.status === 'incomplete') { return false; } const getOrgFromCustomer = await this._organizationService.getOrgByCustomerId( event.data.object.customer as string ); if (!getOrgFromCustomer?.allowTrial) { return true; } console.log('Checking card'); const paymentMethods = await stripe.paymentMethods.list({ customer: event.data.object.customer as string, }); // find the last one created const latestMethod = paymentMethods.data.reduce( (prev, current) => { if (prev.created < current.created) { return current; } return prev; }, { created: -100 } as Stripe.PaymentMethod ); if (!latestMethod.id) { return false; } try { const paymentIntent = await stripe.paymentIntents.create({ amount: 100, currency: 'usd', payment_method: latestMethod.id, customer: event.data.object.customer as string, automatic_payment_methods: { allow_redirects: 'never', enabled: true, }, capture_method: 'manual', // Authorize without capturing confirm: true, // Confirm the PaymentIntent }); if (paymentIntent.status !== 'requires_capture') { console.error('Cant charge'); await stripe.paymentMethods.detach(paymentMethods.data[0].id); await stripe.subscriptions.cancel(event.data.object.id as string); return false; } await stripe.paymentIntents.cancel(paymentIntent.id as string); return true; } catch (err) { try { await stripe.paymentMethods.detach(paymentMethods.data[0].id); await stripe.subscriptions.cancel(event.data.object.id as string); } catch (err) { /*dont do anything*/ } return false; } } async createSubscription(event: Stripe.CustomerSubscriptionCreatedEvent) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const { uniqueId, billing, period, }: { billing: 'STANDARD' | 'PRO'; period: 'MONTHLY' | 'YEARLY'; uniqueId: string; } = event.data.object.metadata; try { const check = await this.checkValidCard(event); if (!check) { return { ok: false }; } } catch (err) { return { ok: false }; } return this._subscriptionService.createOrUpdateSubscription( event.data.object.status !== 'active', uniqueId, event.data.object.customer as string, pricing[billing].channel!, billing, period, event.data.object.cancel_at ); } async updateSubscription(event: Stripe.CustomerSubscriptionUpdatedEvent) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const { uniqueId, billing, period, }: { billing: 'STANDARD' | 'PRO'; period: 'MONTHLY' | 'YEARLY'; uniqueId: string; } = event.data.object.metadata; const check = await this.checkValidCard(event); if (!check) { return { ok: false }; } return this._subscriptionService.createOrUpdateSubscription( event.data.object.status !== 'active', uniqueId, event.data.object.customer as string, pricing[billing].channel!, billing, period, event.data.object.cancel_at ); } async deleteSubscription(event: Stripe.CustomerSubscriptionDeletedEvent) { await this._subscriptionService.deleteSubscription( event.data.object.customer as string ); } async createOrGetCustomer(organization: Organization) { if (organization.paymentId) { return organization.paymentId; } const users = await this._organizationService.getTeam(organization.id); const customer = await stripe.customers.create({ email: users.users[0].user.email, name: organization.name, }); await this._subscriptionService.updateCustomerId( organization.id, customer.id ); return customer.id; } async getPackages() { const products = await stripe.prices.list({ active: true, expand: ['data.tiers', 'data.product'], lookup_keys: [ 'standard_monthly', 'standard_yearly', 'pro_monthly', 'pro_yearly', ], }); const productsList = groupBy( products.data.map((p) => ({ // @ts-ignore name: p.product?.name, recurring: p?.recurring?.interval!, price: p?.tiers?.[0]?.unit_amount! / 100, })), 'recurring' ); return { ...productsList }; } async prorate(organizationId: string, body: BillingSubscribeDto) { const org = await this._organizationService.getOrgById(organizationId); const customer = await this.createOrGetCustomer(org!); const priceData = pricing[body.billing]; const allProducts = await stripe.products.list({ active: true, expand: ['data.prices'], }); const findProduct = allProducts.data.find( (product) => product.name.toUpperCase() === body.billing.toUpperCase() ) || (await stripe.products.create({ active: true, name: body.billing, })); const pricesList = await stripe.prices.list({ active: true, product: findProduct!.id, }); const findPrice = pricesList.data.find( (p) => p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') && p?.nickname === body.billing + ' ' + body.period && p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100 ) || (await stripe.prices.create({ active: true, product: findProduct!.id, currency: 'usd', nickname: body.billing + ' ' + body.period, unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100, recurring: { interval: body.period === 'MONTHLY' ? 'month' : 'year', }, })); const proration_date = Math.floor(Date.now() / 1000); const currentUserSubscription = { data: ( await stripe.subscriptions.list({ customer, status: 'all', }) ).data.filter((f) => f.status === 'active' || f.status === 'trialing'), }; try { const price = await stripe.invoices.retrieveUpcoming({ customer, subscription: currentUserSubscription?.data?.[0]?.id, subscription_proration_behavior: 'create_prorations', subscription_billing_cycle_anchor: 'now', subscription_items: [ { id: currentUserSubscription?.data?.[0]?.items?.data?.[0]?.id, price: findPrice?.id!, quantity: 1, }, ], subscription_proration_date: proration_date, }); return { price: price?.amount_remaining ? price?.amount_remaining / 100 : 0, }; } catch (err) { return { price: 0 }; } } async getCustomerSubscriptions(organizationId: string) { const org = (await this._organizationService.getOrgById(organizationId))!; const customer = org.paymentId; return stripe.subscriptions.list({ customer: customer!, status: 'all', }); } async setToCancel(organizationId: string) { const id = makeId(10); const org = await this._organizationService.getOrgById(organizationId); const customer = await this.createOrGetCustomer(org!); const currentUserSubscription = { data: ( await stripe.subscriptions.list({ customer, status: 'all', }) ).data.filter((f) => f.status !== 'canceled'), }; const { cancel_at } = await stripe.subscriptions.update( currentUserSubscription.data[0].id, { cancel_at_period_end: !currentUserSubscription.data[0].cancel_at_period_end, metadata: { service: 'gitroom', id, }, } ); return { id, cancel_at: cancel_at ? new Date(cancel_at * 1000) : undefined, }; } async getCustomerByOrganizationId(organizationId: string) { const org = (await this._organizationService.getOrgById(organizationId))!; return org.paymentId; } async createBillingPortalLink(customer: string) { return stripe.billingPortal.sessions.create({ customer, return_url: process.env['FRONTEND_URL'] + '/billing', }); } private async createEmbeddedCheckout( ud: string, uniqueId: string, customer: string, body: BillingSubscribeDto, price: string, userId: string, allowTrial: boolean ) { const stripeCustom = new Stripe(process.env.STRIPE_SECRET_KEY!, { // @ts-ignore apiVersion: '2025-03-31.basil', }); if (body.dub) { await stripeCustom.customers.update(customer, { metadata: { dubCustomerExternalId: userId, dubClickId: body.dub, }, }); } const isUtm = body.utm ? `&utm_source=${body.utm}` : ''; // @ts-ignore const { client_secret } = await stripeCustom.checkout.sessions.create({ // @ts-ignore ui_mode: 'custom', customer, return_url: process.env['FRONTEND_URL'] + `/launches?onboarding=true&check=${uniqueId}${isUtm}`, mode: 'subscription', subscription_data: { ...(allowTrial ? { trial_period_days: 7 } : {}), metadata: { service: 'gitroom', ...body, userId, uniqueId, ud, }, }, allow_promotion_codes: body.period === 'MONTHLY', line_items: [ { price, quantity: 1, }, ], }); return { client_secret }; } private async createCheckoutSession( ud: string, uniqueId: string, customer: string, body: BillingSubscribeDto, price: string, userId: string, allowTrial: boolean ) { const isUtm = body.utm ? `&utm_source=${body.utm}` : ''; if (body.dub) { await stripe.customers.update(customer, { metadata: { dubCustomerExternalId: userId, dubClickId: body.dub, }, }); } const { url } = await stripe.checkout.sessions.create({ customer, cancel_url: process.env['FRONTEND_URL'] + `/billing?cancel=true${isUtm}`, success_url: process.env['FRONTEND_URL'] + `/launches?onboarding=true&check=${uniqueId}${isUtm}`, mode: 'subscription', subscription_data: { ...(allowTrial ? { trial_period_days: 7 } : {}), metadata: { service: 'gitroom', ...body, userId, uniqueId, ud, }, }, allow_promotion_codes: body.period === 'MONTHLY', line_items: [ { price, quantity: 1, }, ], }); return { url }; } async createAccountProcess(userId: string, email: string, country: string) { const account = await this._subscriptionService.getUserAccount(userId); if (account?.account && account?.connectedAccount) { return { url: await this.addBankAccount(account.account) }; } if (account?.account && !account?.connectedAccount) { await stripe.accounts.del(account.account); } const createAccount = await this.createAccount(userId, email, country); return { url: await this.addBankAccount(createAccount) }; } async createAccount(userId: string, email: string, country: string) { const account = await stripe.accounts.create({ type: 'custom', capabilities: { transfers: { requested: true, }, card_payments: { requested: true, }, }, tos_acceptance: { service_agreement: 'full', }, metadata: { service: 'gitroom', }, country, email, }); await this._subscriptionService.updateAccount(userId, account.id); return account.id; } async addBankAccount(userId: string) { const accountLink = await stripe.accountLinks.create({ account: userId, refresh_url: process.env['FRONTEND_URL'] + '/marketplace/seller', return_url: process.env['FRONTEND_URL'] + '/marketplace/seller', type: 'account_onboarding', collection_options: { fields: 'eventually_due', }, }); return accountLink.url; } async finishTrial(paymentId: string) { const list = ( await stripe.subscriptions.list({ customer: paymentId, }) ).data.filter((f) => f.status === 'trialing'); return stripe.subscriptions.update(list[0].id, { trial_end: 'now', }); } async checkDiscount(customer: string) { if (!process.env.STRIPE_DISCOUNT_ID) { return false; } const list = await stripe.charges.list({ customer, limit: 1, }); if (!list.data.filter((f) => f.amount > 1000).length) { return false; } const currentUserSubscription = { data: ( await stripe.subscriptions.list({ customer, status: 'all', expand: ['data.discounts'], }) ).data.find((f) => f.status === 'active' || f.status === 'trialing'), }; if (!currentUserSubscription) { return false; } if ( currentUserSubscription.data?.items.data[0]?.price.recurring?.interval === 'year' || currentUserSubscription.data?.discounts.length ) { return false; } return true; } async applyDiscount(customer: string) { const check = this.checkDiscount(customer); if (!check) { return false; } const currentUserSubscription = { data: ( await stripe.subscriptions.list({ customer, status: 'all', expand: ['data.discounts'], }) ).data.find((f) => f.status === 'active' || f.status === 'trialing'), }; await stripe.subscriptions.update(currentUserSubscription.data.id, { discounts: [ { coupon: process.env.STRIPE_DISCOUNT_ID!, }, ], }); return true; } async checkSubscription(organizationId: string, subscriptionId: string) { const orgValue = await this._subscriptionService.checkSubscription( organizationId, subscriptionId ); if (orgValue) { return 2; } const getCustomerSubscriptions = await this.getCustomerSubscriptions( organizationId ); if (getCustomerSubscriptions.data.length === 0) { return 0; } if ( getCustomerSubscriptions.data.find( (p) => p.metadata.uniqueId === subscriptionId )?.canceled_at ) { return 1; } return 0; } async payAccountStepOne( userId: string, organization: Organization, seller: User, orderId: string, ordersItems: Array<{ integrationType: string; quantity: number; price: number; }>, groupId: string ) { const customer = (await this.createOrGetCustomer(organization))!; const price = ordersItems.reduce((all, current) => { return all + current.price * current.quantity; }, 0); const { url } = await stripe.checkout.sessions.create({ customer, mode: 'payment', currency: 'usd', success_url: process.env['FRONTEND_URL'] + `/messages/${groupId}`, metadata: { orderId, service: 'gitroom', type: 'marketplace', }, line_items: [ ...ordersItems, { integrationType: `Gitroom Fee (${+process.env.FEE_AMOUNT! * 100}%)`, quantity: 1, price: price * +process.env.FEE_AMOUNT!, }, ].map((item) => ({ price_data: { currency: 'usd', product_data: { // @ts-ignore name: (!item.price ? 'Platform: ' : '') + capitalize(item.integrationType), }, // @ts-ignore unit_amount: item.price * 100, }, quantity: item.quantity, })), payment_intent_data: { transfer_group: orderId, }, }); return { url }; } async embedded( uniqueId: string, organizationId: string, userId: string, body: BillingSubscribeDto, allowTrial: boolean ) { const id = makeId(10); const priceData = pricing[body.billing]; const org = await this._organizationService.getOrgById(organizationId); const customer = await this.createOrGetCustomer(org!); const allProducts = await stripe.products.list({ active: true, expand: ['data.prices'], }); const findProduct = allProducts.data.find( (product) => product.name.toUpperCase() === body.billing.toUpperCase() ) || (await stripe.products.create({ active: true, name: body.billing, })); const pricesList = await stripe.prices.list({ active: true, product: findProduct!.id, }); const findPrice = pricesList.data.find( (p) => p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') && p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100 ) || (await stripe.prices.create({ active: true, product: findProduct!.id, currency: 'usd', nickname: body.billing + ' ' + body.period, unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100, recurring: { interval: body.period === 'MONTHLY' ? 'month' : 'year', }, })); return this.createEmbeddedCheckout( uniqueId, id, customer, body, findPrice!.id, userId, allowTrial ); } async subscribe( uniqueId: string, organizationId: string, userId: string, body: BillingSubscribeDto, allowTrial: boolean ) { const id = makeId(10); const priceData = pricing[body.billing]; const org = await this._organizationService.getOrgById(organizationId); const customer = await this.createOrGetCustomer(org!); const allProducts = await stripe.products.list({ active: true, expand: ['data.prices'], }); const findProduct = allProducts.data.find( (product) => product.name.toUpperCase() === body.billing.toUpperCase() ) || (await stripe.products.create({ active: true, name: body.billing, })); const pricesList = await stripe.prices.list({ active: true, product: findProduct!.id, }); const findPrice = pricesList.data.find( (p) => p?.recurring?.interval?.toLowerCase() === (body.period === 'MONTHLY' ? 'month' : 'year') && p?.unit_amount === (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100 ) || (await stripe.prices.create({ active: true, product: findProduct!.id, currency: 'usd', nickname: body.billing + ' ' + body.period, unit_amount: (body.period === 'MONTHLY' ? priceData.month_price : priceData.year_price) * 100, recurring: { interval: body.period === 'MONTHLY' ? 'month' : 'year', }, })); const getCurrentSubscriptions = await this._subscriptionService.getSubscription(organizationId); if (!getCurrentSubscriptions) { return this.createCheckoutSession( uniqueId, id, customer, body, findPrice!.id, userId, allowTrial ); } const currentUserSubscription = { data: ( await stripe.subscriptions.list({ customer, status: 'all', }) ).data.filter((f) => f.status === 'active' || f.status === 'trialing'), }; try { await stripe.subscriptions.update(currentUserSubscription.data[0].id, { cancel_at_period_end: false, metadata: { service: 'gitroom', ...body, userId, id, ud: uniqueId, }, proration_behavior: 'always_invoice', items: [ { id: currentUserSubscription.data[0].items.data[0].id, price: findPrice!.id, quantity: 1, }, ], }); return { id }; } catch (err) { const { url } = await this.createBillingPortalLink(customer); return { portal: url, }; } } async paymentSucceeded(event: Stripe.InvoicePaymentSucceededEvent) { // get subscription from payment const subscription = await stripe.subscriptions.retrieve( event.data.object.subscription as string ); const { userId, ud } = subscription.metadata; const user = await this._userService.getUserById(userId); if (user && user.ip && user.agent) { this._trackService.track(ud, user.ip, user.agent, TrackEnum.Purchase, { value: event.data.object.amount_paid / 100, }); } return { ok: true }; } async payout( orderId: string, charge: string, account: string, price: number ) { return stripe.transfers.create({ amount: price * 100, currency: 'usd', destination: account, source_transaction: charge, transfer_group: orderId, }); } async lifetimeDeal(organizationId: string, code: string) { const getCurrentSubscription = await this._subscriptionService.getSubscriptionByOrganizationId( organizationId ); if (getCurrentSubscription && !getCurrentSubscription?.isLifetime) { throw new Error('You already have a non lifetime subscription'); } try { const testCode = AuthService.fixedDecryption(code); const findCode = await this._subscriptionService.getCode(testCode); if (findCode) { return { success: false, }; } const nextPackage = !getCurrentSubscription ? 'STANDARD' : 'PRO'; const findPricing = pricing[nextPackage]; await this._subscriptionService.createOrUpdateSubscription( false, makeId(10), organizationId, getCurrentSubscription?.subscriptionTier === 'PRO' ? getCurrentSubscription.totalChannels + 5 : findPricing.channel!, nextPackage, 'MONTHLY', null, testCode, organizationId ); return { success: true, }; } catch (err) { console.log(err); return { success: false, }; } } }