#jonas_unexpected

1 messages ¡ Page 1 of 1 (latest)

turbid forgeBOT
#

👋 Welcome to your new thread!

⏲️ We'll be here soon! Typically we respond in a few minutes, but sometimes we might take a bit longer if the server is busy or if you have a particularly tricky question.

⏱️ We close idle threads, which makes them read-only. Once a thread is closed it won't be reopened, but you can always start a new thread if you have another question.

🔗 This thread will always be available, even after it's closed. You can find it again using Discord's search, or you can save this link: https://discord.com/channels/841573134531821608/1348763016744927262

📝 Have more to share? Add more details, code, screenshots, videos, etc. below.

frigid igloo
#

Hi there

#

Can you provide more details? It'll be helpful if you can share a Customer ID and clarify how you integrate with Stripe, e.g. Payment Links, Checkout, PaymentElement, etc.

lethal dagger
#

This is my backend code in NestJS:

async createCheckoutSession(request: Request, planName: Role) {
    const priceId = await getPlanAndPrice(this.stripe, planName);

    const session = await this.stripe.checkout.sessions
      .create({
        ui_mode: 'embedded',
        payment_method_types: ['card', 'paypal'],
        line_items: [
          {
            price: priceId.price,
            quantity: 1,
          },
        ],
        mode: 'subscription',
        billing_address_collection: 'required',
        automatic_tax: { enabled: true },
        return_url: `${request.headers.origin}/return?session_id={CHECKOUT_SESSION_ID}`,
      })
      .catch(() => {
        throw new ConflictException('Error creating checkout session');
      });

    return { sessionId: session.id, client_secret: session.client_secret };
  }

  async getSessionStatus(sessionId: string, userId: number) {
    try {
      const session = await this.stripe.checkout.sessions.retrieve(sessionId);
      const customer = {
        id: session.customer,
        customer_details: session.customer_details,
      };
      const subscription = session.subscription;
      const subscriptionInfo = await this.stripe.subscriptions.retrieve(
        typeof subscription === 'string' ? subscription : subscription.id,
      );
      const product = await this.stripe.products.retrieve(
        subscriptionInfo.items.data[0].plan.product as string,
      );

      console.log('[STRIPE subscription]: ', subscription);
      console.log('[STRIPE subscriptionInfo]: ', subscriptionInfo);

      //Update user role and subscription
      if (session.payment_status === 'paid') {
        await handleSubscription(
          this.prisma,
          userId,
          customer.id as string,
          subscriptionInfo.id,
          product.name.toLocaleUpperCase() as Role,
        );
      }

      const { phone, tax_exempt, tax_ids, ...customer_details } =
        customer.customer_details;

      return {
        status: session.payment_status,
        customer: customer_details,
        product: { name: product.name, price: session.amount_total / 100 },
      };
    } catch (error) {
      if (error.type === 'StripeInvalidRequestError')
        throw new NotFoundException('Session not found');

      console.log('[STRIPE getSessionStatus]: ', error);
      throw new InternalServerErrorException('Error getting session status');
    }
  }

This is my Frontend code

"use client";
import { FC, useEffect, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
  EmbeddedCheckoutProvider,
  EmbeddedCheckout,
} from "@stripe/react-stripe-js";
import { STRIPE_PUBLIC_KEY } from "@/lib/constants";
import { Role } from "@/types/user";
import ApiClient from "@/api";

interface CheckoutProps {
  plan: Role | null;
}

const apiClient = new ApiClient();

const Checkout: FC<CheckoutProps> = ({ plan }) => {
  const [clientSecret, setClientSecret] = useState<string | null>(null);
  const stripePromise = loadStripe(STRIPE_PUBLIC_KEY);

  useEffect(() => {
    apiClient.stripe.helper.createCheckoutSession(plan).then((response) => {
      if (response.status) {
        setClientSecret(response.data.client_secret);
      }
    });
  }, [plan]);

  return (
    <EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
      <div className="text-center">
        <h1 className="text-4xl font-bold mb-4 text-blackho">
          Subscribe to {plan}
        </h1>
        <p className="text-xl text-gray-600 max-w-2xl mx-auto">
          You are about to subscribe to the {plan} plan. Please enter your
          payment details below to proceed.
        </p>

        <div className="mt-8">
          <EmbeddedCheckout />
        </div>
      </div>
    </EmbeddedCheckoutProvider>
  );
};

export default Checkout;

#

helper.ts:

async createCheckoutSession(plan: Role | null) {
    return axios
      .post(`payment/create-checkout-session?plan=${plan}`)
      .then((response) => {
        if (response.status !== 201) return { data: null, status: false };

        const data = response.data;
        return { data: data, status: true };
      }
frigid igloo
#

Okay, you're creating Checkout Sessions in subscription mode. These Checkout Sessions will automatically create a Customer object with the details included in the session (e.g. email address)

lethal dagger
#

yeah

frigid igloo
#

Email addresses on Stripe are not unique. This means that it's possible for multiple Customer objects to be created with the same email address. I'm sure if you click into those three Customers in your Dashboard, you'll see they'll all have unique IDs that begin with cus_

lethal dagger
#

yes they have others IDs. But how am I supposed to look at the billing history of a user if a new customer is created again and again?

frigid igloo
#

If a Customer object exists for a given customer of yours, you should use their Customer ID when creating the Checkout Session if you want to associate payment from that Sesssion (either a one-off payment or a Subscription) with their existing Customer object on Stripe.

#

It's not possible to merge existing Customer objects into one so if some of your customers already have multiple Customer objects on Stripe, you'll need to handle any history manually/look at all of the Customer objects together on your end

lethal dagger
#

So I have to program the logic behind it myself first. I can't just set this up in the Stripe dashboard?

frigid igloo
#

What logic exactly?

lethal dagger
#

That I bind several invoices to one customer

frigid igloo
#

Stripe can handle this for you for all new customers going forward. You'd just need to ensure any Checkout Sessions you create for existing customers include their Customer ID.

lethal dagger
#

How do I integrate the customer id into the payment process?

frigid igloo
#

For any existing customers, you're kind of stuck with multiple customer objects if they've already been created. If you want to use one customer object going forward, you'll essentially need to pick one as the "real" one then cancel any Subscriptions on the other customer objects and recreate them on the "real" one. I recommend recreating this scenario and testing this in test mode first to make sure you get it right

lethal dagger
#

Ok then once again to conclude.

What I want: I want each subscription to be linked to the same customer and not create a new customer with the same data for each new subscription.

It is not possible to implement this directly in Stripe. Is that right?

That means I have to program the logic into my program myself?

frigid igloo
#

It is possible but yes, you'll also need some logic on your end.

lethal dagger
#

So is this the rigth way?

async createCheckoutSession(request: Request, planName: Role) {
    const priceId = await getPlanAndPrice(this.stripe, planName);

    const customerId = 'cus_1234567890';

    const session = await this.stripe.checkout.sessions
      .create({
        ui_mode: 'embedded',
        payment_method_types: ['card', 'paypal'],
        line_items: [
          {
            price: priceId.price,
            quantity: 1,
          },
        ],
        mode: 'subscription',
        billing_address_collection: 'required',
        automatic_tax: { enabled: true },
        customer: customerId,
        return_url: `${request.headers.origin}/return?session_id={CHECKOUT_SESSION_ID}`,
      })
      .catch(() => {
        throw new ConflictException('Error creating checkout session');
      });

    return { sessionId: session.id, client_secret: session.client_secret };
  }
frigid igloo
#

Is cus_1234567890 just a placeholder in this case?

lethal dagger
#

yes

frigid igloo
#

Got it, so yes, that's what you want

#

Lets' say I'm a new customer of yours. If I land on your shop and go to check out, there should ideally be a way for me to register on your site. When I register, you should have some logic to create some user on your end and also create a Stripe Customer object. You should keep track of the mapping between your user ID and the Stripe Customer ID.

#

If I return to your shop in a month because I want to place a similar order, the Checkout Session you create after I log in should include customer: cus_1234567890 if cus_1234567890 is the ID of the Customer object that was created when I first registered

lethal dagger
#

This is my prisma.schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

enum Role {
  ADMIN
  VIP
  PREMIUM
  USER
}

model User {
  id        Int      @id @default(autoincrement())
  username  String
  email     String   @unique
  password  String
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  clientKey    ClientKey?
  Subscription Subscription?
}


model ClientKey {
  id        Int      @id @default(autoincrement())
  key       String   @unique
  createdAt DateTime @default(now())

  userId Int  @unique
  user   User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Subscription {
  id             String   @id @default(cuid())
  customerId     String
  subscriptionId String
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  userId Int  @unique
  user   User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

#

When the users makes a payment then the Subscription is created

#
export const handleSubscription = async (
  prisma: PrismaService,
  userId: number,
  customerId: string,
  subscriptionId: string,
  role: Role,
) => {
  const user = await prisma.user.findUnique({ where: { id: userId } });

  if (!user) throw new NotFoundException('User not found');

  await prisma.subscription.upsert({
    where: {
      userId: userId,
    },
    create: {
      userId: userId,
      customerId: customerId,
      subscriptionId: subscriptionId,
    },
    update: {
      customerId: customerId,
      subscriptionId: subscriptionId,
    },
  });

  await prisma.user.update({
    where: {
      id: userId,
    },
    data: {
      role: role,
    },
  });
};
frigid igloo
#

What's your question?

frigid igloo
#

@lethal dagger let me know if you can clarify any remaining questions you have about this functionality