mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-20 07:16:17 +08:00
add subscription status
This commit is contained in:
parent
383a0be958
commit
f6e63cf871
|
@ -308,11 +308,11 @@ describe("store controller test", () => {
|
|||
expect(stripeGetCheckoutMock).toHaveBeenCalledWith("sessionId");
|
||||
expect(userLinkCustomerByUidMock).toHaveBeenCalledWith(uid, "customerId");
|
||||
expect(stripeGetSubscriptionMock).toHaveBeenCalledWith("subscriptionId");
|
||||
expect(userUpdatePremiumMock).toHaveBeenCalledWith(
|
||||
"customerId",
|
||||
10000,
|
||||
20000
|
||||
);
|
||||
expect(userUpdatePremiumMock).toHaveBeenCalledWith("customerId", {
|
||||
startTimestamp: 10000,
|
||||
expirationTimestamp: 20000,
|
||||
subscriptionStatus: "active",
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail for mismatch user", async () => {
|
||||
|
@ -482,11 +482,49 @@ describe("store controller test", () => {
|
|||
|
||||
//THEN
|
||||
expect(stripeGetSubscriptionMock).toHaveBeenCalledWith("sub_1234");
|
||||
expect(userUpdatePremiumMock).toHaveBeenCalledWith(
|
||||
"cus_1234",
|
||||
10 * 1000,
|
||||
20 * 1000
|
||||
);
|
||||
expect(userUpdatePremiumMock).toHaveBeenCalledWith("cus_1234", {
|
||||
startTimestamp: 10 * 1000,
|
||||
expirationTimestamp: 20 * 1000,
|
||||
subscriptionStatus: "active",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("event type customer.subscription", () => {
|
||||
afterEach(async () => {
|
||||
[userUpdatePremiumMock].forEach((it) => it.mockReset());
|
||||
});
|
||||
|
||||
it("should handle customer.subscription.deleted", async () => {
|
||||
//GIVEN
|
||||
const event = {
|
||||
type: "customer.subscription.deleted",
|
||||
data: {
|
||||
object: {
|
||||
id: "sub_1234",
|
||||
customer: "cus_1234",
|
||||
start_date: 10,
|
||||
current_period_end: 20,
|
||||
status: "canceled",
|
||||
},
|
||||
},
|
||||
} as Stripe.WebhookEvent;
|
||||
|
||||
userUpdatePremiumMock.mockResolvedValue();
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/store/webhook")
|
||||
.set("stripe-signature", "validSignature")
|
||||
.send(event)
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(userUpdatePremiumMock).toHaveBeenCalledWith("cus_1234", {
|
||||
startTimestamp: 10 * 1000,
|
||||
expirationTimestamp: 20 * 1000,
|
||||
subscriptionStatus: "canceled",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -857,16 +857,25 @@ describe("UserDal", () => {
|
|||
await UserDAL.linkStripeCustomerIdByUid(uid, customerId);
|
||||
|
||||
//WHEN
|
||||
await UserDAL.updatePremiumByStripeCustomerId(customerId, 10, 20);
|
||||
await UserDAL.updatePremiumByStripeCustomerId(customerId, {
|
||||
startTimestamp: 10,
|
||||
expirationTimestamp: 20,
|
||||
subscriptionStatus: "active",
|
||||
});
|
||||
|
||||
//THEN
|
||||
const readUser = await UserDAL.getUser(uid, "test");
|
||||
expect(readUser).toHaveProperty("premium.startTimestamp", 10);
|
||||
expect(readUser).toHaveProperty("premium.expirationTimestamp", 20);
|
||||
expect(readUser).toHaveProperty("premium.subscriptionStatus", "active");
|
||||
});
|
||||
it("should fail for unknown customerId", async () => {
|
||||
await expect(
|
||||
UserDAL.updatePremiumByStripeCustomerId("unknownCustomerId", 10, 20)
|
||||
UserDAL.updatePremiumByStripeCustomerId("unknownCustomerId", {
|
||||
startTimestamp: 10,
|
||||
expirationTimestamp: 20,
|
||||
subscriptionStatus: "active",
|
||||
})
|
||||
).rejects.toThrow(new MonkeyError(404, "Cannot update premium info."));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -75,7 +75,9 @@ export async function finalizeCheckout(
|
|||
|
||||
switch (session.mode) {
|
||||
case "subscription":
|
||||
await processSubscription(session.subscription as string);
|
||||
await processSubscription(
|
||||
await fetchSubscription(session.subscription as string)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new MonkeyError(
|
||||
|
@ -87,21 +89,27 @@ export async function finalizeCheckout(
|
|||
return new MonkeyResponse("Checkout finalized", {});
|
||||
}
|
||||
|
||||
async function processSubscription(subscriptionId: string): Promise<void> {
|
||||
const subscription = await Stripe.getSubscription(subscriptionId);
|
||||
|
||||
if (subscription.status === "active") {
|
||||
async function fetchSubscription(
|
||||
subscriptionId: string
|
||||
): Promise<Stripe.Subscription> {
|
||||
return await Stripe.getSubscription(subscriptionId);
|
||||
}
|
||||
async function processSubscription(
|
||||
subscription: Stripe.Subscription
|
||||
): Promise<void> {
|
||||
if (subscription.status === "active" || subscription.status === "canceled") {
|
||||
//
|
||||
const startDate = subscription.start_date * 1000;
|
||||
const endDate = subscription.current_period_end * 1000;
|
||||
const startTimestamp = subscription.start_date * 1000;
|
||||
const expirationTimestamp = subscription.current_period_end * 1000;
|
||||
|
||||
await UserDal.updatePremiumByStripeCustomerId(
|
||||
subscription.customer as string,
|
||||
startDate,
|
||||
endDate
|
||||
{
|
||||
startTimestamp,
|
||||
expirationTimestamp,
|
||||
subscriptionStatus: subscription.status,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
//we don't need to handle other states as premium validity is calculated based on the expirationTimestamp.
|
||||
}
|
||||
}
|
||||
export async function handleWebhook(
|
||||
|
@ -120,7 +128,7 @@ export async function handleWebhook(
|
|||
await processInvoicePaid(event.data.object);
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
//TODO implement
|
||||
await processSubscriptionDeleted(event.data.object);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -139,5 +147,11 @@ async function processCustomerCreated(
|
|||
|
||||
async function processInvoicePaid(invoice: Stripe.Invoice): Promise<void> {
|
||||
const subscriptionId = invoice.subscription as string;
|
||||
await processSubscription(subscriptionId);
|
||||
const subscription = await fetchSubscription(subscriptionId);
|
||||
await processSubscription(subscription);
|
||||
}
|
||||
async function processSubscriptionDeleted(
|
||||
subscription: Stripe.Subscription
|
||||
): Promise<void> {
|
||||
await processSubscription(subscription);
|
||||
}
|
||||
|
|
|
@ -1107,14 +1107,13 @@ async function linkStripeCustomer(
|
|||
}
|
||||
export async function updatePremiumByStripeCustomerId(
|
||||
customerId: string,
|
||||
startDate: number,
|
||||
endDate: number
|
||||
premium: MonkeyTypes.PremiumInfo
|
||||
): Promise<void> {
|
||||
const result = await getUsersCollection().updateOne(
|
||||
{ "stripeData.customerId": customerId },
|
||||
{
|
||||
$set: {
|
||||
premium: { startTimestamp: startDate, expirationTimestamp: endDate },
|
||||
premium,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
1
backend/src/types/types.d.ts
vendored
1
backend/src/types/types.d.ts
vendored
|
@ -400,6 +400,7 @@ declare namespace MonkeyTypes {
|
|||
interface PremiumInfo {
|
||||
startTimestamp: number;
|
||||
expirationTimestamp: number;
|
||||
subscriptionStatus: "active" | "canceled";
|
||||
}
|
||||
|
||||
interface StripeData {
|
||||
|
|
|
@ -45,6 +45,7 @@ async function fill(): Promise<void> {
|
|||
}
|
||||
|
||||
if (data.isPremium === true) {
|
||||
let premiumStartDate = "";
|
||||
let premiumEndDate = "";
|
||||
if (data.premium?.expirationTimestamp) {
|
||||
if (data.premium?.expirationTimestamp === -1) {
|
||||
|
@ -54,10 +55,24 @@ async function fill(): Promise<void> {
|
|||
premiumEndDate = new Date(
|
||||
data.premium?.expirationTimestamp
|
||||
).toDateString();
|
||||
premiumStartDate = new Date(
|
||||
data.premium?.startTimestamp
|
||||
).toDateString();
|
||||
}
|
||||
$("#premium_from").html(premiumStartDate);
|
||||
$("#premium_until").html(premiumEndDate);
|
||||
}
|
||||
|
||||
if (data.premium?.subscriptionStatus === "active") {
|
||||
$("#premium_renewal").html(
|
||||
` Subscription will auto-renewal on ${premiumEndDate}`
|
||||
);
|
||||
} else {
|
||||
$("#premium_renewal").html(
|
||||
` Subscription is pending cancellation until ${premiumEndDate}.`
|
||||
);
|
||||
}
|
||||
|
||||
$(".premiumActive").removeClass("hidden");
|
||||
} else {
|
||||
$(".premiumActive").addClass("hidden");
|
||||
|
|
1
frontend/src/ts/types/types.d.ts
vendored
1
frontend/src/ts/types/types.d.ts
vendored
|
@ -924,5 +924,6 @@ declare namespace MonkeyTypes {
|
|||
interface PremiumInfo {
|
||||
startTimestamp: number;
|
||||
expirationTimestamp: number;
|
||||
subscriptionStatus: "active" | "canceled";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
<div class="premiumActive hidden">
|
||||
<h2>You are a premium user</h2>
|
||||
<p>
|
||||
premium active until
|
||||
premium active from
|
||||
<span id="premium_from"></span>
|
||||
until
|
||||
<span id="premium_until"></span>
|
||||
.
|
||||
<br />
|
||||
<span id="premium_renewal"></span>
|
||||
</p>
|
||||
<input
|
||||
type="button"
|
||||
|
|
Loading…
Reference in a new issue