allow organizations to have multiple subscriptions. although only 1 can be active at a time

This commit is contained in:
m5r 2021-10-03 18:19:45 +02:00
parent 22e2b21b14
commit 3a3d526e77
6 changed files with 65 additions and 37 deletions

View File

@ -1,6 +1,6 @@
import { Queue } from "quirrel/blitz"; import { Queue } from "quirrel/blitz";
import db, { MembershipRole } from "../../../../db"; import db, { MembershipRole, SubscriptionStatus } from "../../../../db";
import appLogger from "../../../../integrations/logger"; import appLogger from "../../../../integrations/logger";
import { cancelPaddleSubscription } from "../../../../integrations/paddle"; import { cancelPaddleSubscription } from "../../../../integrations/paddle";
@ -17,7 +17,11 @@ const deleteUserData = Queue<Payload>("api/queue/delete-user-data", async ({ use
memberships: { memberships: {
include: { include: {
organization: { organization: {
include: { subscription: true }, include: {
subscriptions: {
where: { status: { not: SubscriptionStatus.deleted } },
},
},
}, },
}, },
}, },
@ -33,8 +37,12 @@ const deleteUserData = Queue<Payload>("api/queue/delete-user-data", async ({ use
await db.organization.delete({ where: { id: organization.id } }); await db.organization.delete({ where: { id: organization.id } });
await db.user.delete({ where: { id: user.id } }); await db.user.delete({ where: { id: user.id } });
if (organization.subscription) { if (organization.subscriptions.length > 0) {
await cancelPaddleSubscription({ subscriptionId: organization.subscription.paddleSubscriptionId }); await Promise.all(
organization.subscriptions.map((subscription) =>
cancelPaddleSubscription({ subscriptionId: subscription.paddleSubscriptionId }),
),
);
} }
break; break;

View File

@ -18,7 +18,7 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
const organization = await db.organization.findFirst({ const organization = await db.organization.findFirst({
where: { id: organizationId }, where: { id: organizationId },
include: { include: {
subscription: true, subscriptions: true,
memberships: { memberships: {
include: { user: true }, include: { user: true },
}, },
@ -28,29 +28,26 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
throw new NotFoundError(); throw new NotFoundError();
} }
const isReturningSubscriber = organization.subscriptions.length > 0;
const orgOwner = organization.memberships.find((membership) => membership.role === MembershipRole.OWNER); const orgOwner = organization.memberships.find((membership) => membership.role === MembershipRole.OWNER);
const email = orgOwner!.user!.email; const email = orgOwner!.user!.email;
await db.organization.update({ await db.subscription.create({
where: { id: organizationId },
data: { data: {
subscription: { organizationId,
create: { paddleSubscriptionId: event.subscriptionId,
paddleSubscriptionId: event.subscriptionId, paddlePlanId: event.productId,
paddlePlanId: event.productId, paddleCheckoutId: event.checkoutId,
paddleCheckoutId: event.checkoutId, nextBillDate: event.nextPaymentDate,
nextBillDate: event.nextPaymentDate, status: translateSubscriptionStatus(event.status),
status: translateSubscriptionStatus(event.status), lastEventTime: event.eventTime,
lastEventTime: event.eventTime, updateUrl: event.updateUrl,
updateUrl: event.updateUrl, cancelUrl: event.cancelUrl,
cancelUrl: event.cancelUrl, currency: event.currency,
currency: event.currency, unitPrice: event.unitPrice,
unitPrice: event.unitPrice,
},
},
}, },
}); });
if (!!organization.subscription) { if (isReturningSubscriber) {
sendEmail({ sendEmail({
subject: "Welcome back to Shellphone", subject: "Welcome back to Shellphone",
body: "Welcome back to Shellphone", body: "Welcome back to Shellphone",
@ -58,15 +55,17 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
}).catch((error) => { }).catch((error) => {
logger.error(error); logger.error(error);
}); });
} else {
sendEmail({ return;
subject: "Welcome to Shellphone",
body: `Welcome to Shellphone`,
recipients: [email],
}).catch((error) => {
logger.error(error);
});
} }
sendEmail({
subject: "Welcome to Shellphone",
body: `Welcome to Shellphone`,
recipients: [email],
}).catch((error) => {
logger.error(error);
});
}); });
export default subscriptionCreatedQueue; export default subscriptionCreatedQueue;

View File

@ -42,6 +42,7 @@ export const subscriptionPaymentSucceededQueue = Queue<Payload>(
}, },
}); });
}, },
{ retry: ["30s", "1m", "5m"] },
); );
export default subscriptionPaymentSucceededQueue; export default subscriptionPaymentSucceededQueue;

View File

@ -5,7 +5,8 @@ import clsx from "clsx";
import useSubscription from "../../hooks/use-subscription"; import useSubscription from "../../hooks/use-subscription";
export default function Plans() { export default function Plans() {
const { subscription, subscribe } = useSubscription(); const { subscription, subscribe, changePlan } = useSubscription();
const hasSubscription = Boolean(subscription);
return ( return (
<div className="mt-6 flex flex-row-reverse flex-wrap-reverse gap-x-4"> <div className="mt-6 flex flex-row-reverse flex-wrap-reverse gap-x-4">
@ -62,8 +63,13 @@ export default function Plans() {
<button <button
disabled={isCurrentTier} disabled={isCurrentTier}
onClick={() => { onClick={() => {
subscribe({ planId: tier.planId }); if (hasSubscription) {
Panelbear.track(`Subscribe to ${tier.title}`); changePlan({ planId: tier.planId });
Panelbear.track(`Subscribe to ${tier.title}`);
} else {
subscribe({ planId: tier.planId, coupon: "groot429" });
Panelbear.track(`Subscribe to ${tier.title}`);
}
}} }}
className={clsx( className={clsx(
!isCurrentTier !isCurrentTier

View File

@ -0,0 +1,14 @@
/*
Warnings:
- Made the column `organizationId` on table `Subscription` required. This step will fail if there are existing NULL values in that column.
*/
-- DropIndex
DROP INDEX "Subscription_organizationId_unique";
-- AlterTable
ALTER TABLE "Subscription" ALTER COLUMN "organizationId" SET NOT NULL;
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "fullName" DROP DEFAULT;

View File

@ -30,7 +30,7 @@ model Organization {
messages Message[] messages Message[]
phoneCalls PhoneCall[] phoneCalls PhoneCall[]
processingPhoneNumbers ProcessingPhoneNumber[] processingPhoneNumbers ProcessingPhoneNumber[]
subscription Subscription? subscriptions Subscription[]
@@unique([id, twilioAccountSid]) @@unique([id, twilioAccountSid])
} }
@ -50,8 +50,8 @@ model Subscription {
nextBillDate DateTime @db.Date nextBillDate DateTime @db.Date
lastEventTime DateTime @db.Timestamp lastEventTime DateTime @db.Timestamp
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: Cascade) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String? organizationId String
} }
enum SubscriptionStatus { enum SubscriptionStatus {
@ -95,7 +95,7 @@ model User {
id String @id @default(uuid()) id String @id @default(uuid())
createdAt DateTime @default(now()) @db.Timestamptz createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz updatedAt DateTime @updatedAt @db.Timestamptz
fullName String @default("") fullName String
email String @unique email String @unique
hashedPassword String? hashedPassword String?
role GlobalRole @default(CUSTOMER) role GlobalRole @default(CUSTOMER)