diff --git a/packages/api/src/controllers/stripe.ts b/packages/api/src/controllers/stripe.ts
index 27d1332f72..ddc08af78d 100644
--- a/packages/api/src/controllers/stripe.ts
+++ b/packages/api/src/controllers/stripe.ts
@@ -1,4 +1,4 @@
-import { Router, Request } from "express";
+import { Router, Request, Response } from "express";
import { db } from "../store";
import { products } from "../config";
import sql from "sql-template-strings";
@@ -186,6 +186,67 @@ const sendUsageRecordToStripe = async (
);
};
+app.post("/apply-coupon", async (req: Request, res: Response) => {
+ const { coupon, stripeCustomerId, stripeCustomerSubscriptionId } = req.body;
+
+ const user = req.user;
+
+ if (
+ user.stripeCustomerId != stripeCustomerId ||
+ user.stripeCustomerSubscriptionId != stripeCustomerSubscriptionId
+ ) {
+ res.status(400);
+ return res.json({ errors: ["invalid customer or subscription"] });
+ }
+
+ const customer = await req.stripe.customers.retrieve(stripeCustomerId);
+
+ if (!customer) {
+ res.status(404);
+ return res.json({ errors: ["customer not found"] });
+ }
+
+ const subscription = await req.stripe.subscriptions.retrieve(
+ stripeCustomerSubscriptionId
+ );
+
+ if (!subscription) {
+ res.status(404);
+ return res.json({ errors: ["subscription not found"] });
+ }
+
+ if (subscription.status === "canceled") {
+ res.status(400);
+ return res.json({ errors: ["subscription is canceled"] });
+ }
+
+ const couponRes = await req.stripe.coupons.retrieve(coupon);
+
+ if (!couponRes) {
+ res.status(404);
+ return res.json({ errors: ["coupon not found"] });
+ }
+
+ const subscriptionWithCoupon = await req.stripe.subscriptions.update(
+ stripeCustomerSubscriptionId,
+ {
+ coupon: coupon,
+ }
+ );
+
+ if (!subscriptionWithCoupon) {
+ res.status(400);
+ return res.json({ errors: ["failed to apply coupon"] });
+ }
+
+ await db.user.update(user.id, {
+ stripeAppliedCouponId: coupon,
+ });
+
+ res.status(200);
+ return res.json(user);
+});
+
// Webhook handler for asynchronous events called by stripe on invoice generation
// https://stripe.com/docs/billing/subscriptions/webhooks
app.post("/webhook", async (req, res) => {
diff --git a/packages/api/src/controllers/user.ts b/packages/api/src/controllers/user.ts
index fce47ca634..db2ce680d3 100644
--- a/packages/api/src/controllers/user.ts
+++ b/packages/api/src/controllers/user.ts
@@ -438,7 +438,9 @@ app.post("/", validatePost("user"), async (req, res) => {
}
let isTest =
- process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development";
+ process.env.NODE_ENV === "test" ||
+ process.env.NODE_ENV === "development" ||
+ process.env.NODE_ENV === "staging";
if (
req.config.requireEmailVerification &&
@@ -617,7 +619,9 @@ app.post("/token", validatePost("user"), async (req, res) => {
);
let isTest =
- process.env.NODE_ENV === "test" || process.env.NODE_ENV === "development";
+ process.env.NODE_ENV === "test" ||
+ process.env.NODE_ENV === "development" ||
+ process.env.NODE_ENV === "staging";
if (
req.config.requireEmailVerification &&
diff --git a/packages/api/src/schema/db-schema.yaml b/packages/api/src/schema/db-schema.yaml
index ef35ef8003..dfbc03e26f 100644
--- a/packages/api/src/schema/db-schema.yaml
+++ b/packages/api/src/schema/db-schema.yaml
@@ -290,6 +290,9 @@ components:
- prod_O9XuWMU1Up6QKf
- prod_OTTbwpzxNLMNSh
- prod_OTTwpzjA4U8B2P
+ stripeAppliedCouponId:
+ type: string
+ description: stripe coupon id
update-customer-payment-method:
type: object
required:
@@ -1122,6 +1125,9 @@ components:
stripeCustomerSubscriptionId:
type: string
example: sub_I29pdyfOTPBkjb
+ stripeAppliedCouponId:
+ type: string
+ description: stripe coupon id
ccLast4:
type: string
example: 1234
diff --git a/packages/www/components/PaymentMethodDialog/index.tsx b/packages/www/components/PaymentMethodDialog/index.tsx
index cfe77c82b7..495ea26320 100644
--- a/packages/www/components/PaymentMethodDialog/index.tsx
+++ b/packages/www/components/PaymentMethodDialog/index.tsx
@@ -258,7 +258,6 @@ const PaymentMethodDialog = ({ invalidateQuery }) => {
/>
-
{
const { user, updateSubscription } = useApi();
+ const { applyCoupon } = useApi();
const [status, setStatus] = useState("initial");
const stripe = useStripe();
const elements = useElements();
@@ -46,6 +47,7 @@ const PlanForm = ({
function createPaymentMethod({
cardElement,
+ coupon,
stripeCustomerId,
stripeCustomerSubscriptionId,
stripeProductId,
@@ -63,6 +65,14 @@ const PlanForm = ({
console.log(result.error);
setStatus("error");
} else {
+ if (coupon && coupon !== "") {
+ applyCoupon({
+ stripeCustomerId,
+ stripeCustomerSubscriptionId,
+ stripeProductId,
+ coupon,
+ });
+ }
updateSubscription({
stripeCustomerId,
stripeCustomerPaymentMethodId: paymentMethod.id,
@@ -171,6 +181,7 @@ const PlanForm = ({
cardElement,
stripeCustomerId: user.stripeCustomerId,
stripeCustomerSubscriptionId: user.stripeCustomerSubscriptionId,
+ coupon: data.coupon,
stripeProductId,
billingDetails: {
name: data.name,
@@ -376,7 +387,23 @@ const PlanForm = ({
/>
-
+
+
+
+
=> {
+ const [res, body] = await context.fetch("/stripe/apply-coupon", {
+ method: "POST",
+ body: JSON.stringify({
+ stripeCustomerId,
+ stripeCustomerSubscriptionId,
+ stripeProductId,
+ coupon,
+ }),
+ headers: {
+ "content-type": "application/json",
+ },
+ });
+ setState((state) => ({ ...state, userRefresh: Date.now() }));
+
+ if (res.status !== 201) {
+ return body;
+ }
+
+ return res;
+};
+
export const getSubscription = async (
stripeCustomerSubscriptionId: string
): Promise<[Response, ApiError]> => {