add subscription status

This commit is contained in:
Christian Fehmer 2023-12-06 12:31:59 +01:00
parent 383a0be958
commit f6e63cf871
No known key found for this signature in database
GPG key ID: 7294582286D5F1F4
8 changed files with 110 additions and 29 deletions

View file

@ -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",
});
});
});

View file

@ -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."));
});
});

View file

@ -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);
}

View file

@ -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,
},
}
);

View file

@ -400,6 +400,7 @@ declare namespace MonkeyTypes {
interface PremiumInfo {
startTimestamp: number;
expirationTimestamp: number;
subscriptionStatus: "active" | "canceled";
}
interface StripeData {

View file

@ -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");

View file

@ -924,5 +924,6 @@ declare namespace MonkeyTypes {
interface PremiumInfo {
startTimestamp: number;
expirationTimestamp: number;
subscriptionStatus: "active" | "canceled";
}
}

View file

@ -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"