Skip to content

Commit

Permalink
billing: promo codes
Browse files Browse the repository at this point in the history
  • Loading branch information
gioelecerati committed Nov 8, 2023
1 parent e46e532 commit b5c20eb
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 5 deletions.
63 changes: 62 additions & 1 deletion packages/api/src/controllers/stripe.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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/middleware/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,9 +290,15 @@ function authorizer(params: AuthzParams): RequestHandler {
);
}

let isTest =
process.env.NODE_ENV === "test" ||
process.env.NODE_ENV === "development" ||
process.env.NODE_ENV === "staging";

const verifyEmail =
req.config.requireEmailVerification &&
!params.allowUnverified &&
!isTest &&
user.createdAt > EMAIL_VERIFICATION_CUTOFF_DATE;
if (verifyEmail && !user.emailValid) {
throw new ForbiddenError(
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/schema/db-schema.yaml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit b5c20eb

Please sign in to comment.