Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

billing: promo codes #1965

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
63 changes: 62 additions & 1 deletion 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";
Expand Down Expand Up @@ -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) => {
Expand Down
8 changes: 6 additions & 2 deletions packages/api/src/controllers/user.ts
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 &&
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/schema/db-schema.yaml
Expand Up @@ -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:
Expand Down Expand Up @@ -1122,6 +1125,9 @@ components:
stripeCustomerSubscriptionId:
type: string
example: sub_I29pdyfOTPBkjb
stripeAppliedCouponId:
type: string
description: stripe coupon id
ccLast4:
type: string
example: 1234
Expand Down
1 change: 0 additions & 1 deletion packages/www/components/PaymentMethodDialog/index.tsx
Expand Up @@ -258,7 +258,6 @@ const PaymentMethodDialog = ({ invalidateQuery }) => {
/>
</Box>
</Grid>

<Box
css={{
fontSize: "$1",
Expand Down
29 changes: 28 additions & 1 deletion packages/www/components/PlanForm/index.tsx
Expand Up @@ -32,6 +32,7 @@ const PlanForm = ({
color,
}) => {
const { user, updateSubscription } = useApi();
const { applyCoupon } = useApi();
const [status, setStatus] = useState("initial");
const stripe = useStripe();
const elements = useElements();
Expand All @@ -46,6 +47,7 @@ const PlanForm = ({

function createPaymentMethod({
cardElement,
coupon,
stripeCustomerId,
stripeCustomerSubscriptionId,
stripeProductId,
Expand All @@ -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,
Expand Down Expand Up @@ -171,6 +181,7 @@ const PlanForm = ({
cardElement,
stripeCustomerId: user.stripeCustomerId,
stripeCustomerSubscriptionId: user.stripeCustomerSubscriptionId,
coupon: data.coupon,
stripeProductId,
billingDetails: {
name: data.name,
Expand Down Expand Up @@ -376,7 +387,23 @@ const PlanForm = ({
/>
</Box>
</Grid>

<Box>
<Label
css={{ mb: "$1", display: "block" }}
htmlFor="coupon">
Promo code
</Label>
<TextField
size="2"
ref={register({ required: false })}
placeholder="Promo code"
id="coupon"
name="coupon"
type="text"
css={{ width: "100%", mb: "$2" }}
required
/>
</Box>
<Box
css={{
fontSize: "$1",
Expand Down
27 changes: 27 additions & 0 deletions packages/www/hooks/use-api/endpoints/user.ts
Expand Up @@ -373,6 +373,33 @@ export const updateSubscription = async ({
return res;
};

export const applyCoupon = async ({
coupon,
stripeCustomerId,
stripeCustomerSubscriptionId,
stripeProductId,
}): Promise<[Response, User | ApiError]> => {
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]> => {
Expand Down