allow organizations to have multiple subscriptions. although only 1 can be active at a time
This commit is contained in:
parent
22e2b21b14
commit
3a3d526e77
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -42,6 +42,7 @@ export const subscriptionPaymentSucceededQueue = Queue<Payload>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
{ retry: ["30s", "1m", "5m"] },
|
||||||
);
|
);
|
||||||
|
|
||||||
export default subscriptionPaymentSucceededQueue;
|
export default subscriptionPaymentSucceededQueue;
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user