always create new subscription
This commit is contained in:
parent
0d7e0ba1b4
commit
dd9d15d042
@ -1,7 +1,7 @@
|
|||||||
import type { BlitzApiHandler } from "blitz";
|
import type { BlitzApiHandler } from "blitz";
|
||||||
|
|
||||||
import { cancelPaddleSubscription } from "../../../integrations/paddle";
|
import { cancelPaddleSubscription } from "integrations/paddle";
|
||||||
import appLogger from "../../../integrations/logger";
|
import appLogger from "integrations/logger";
|
||||||
|
|
||||||
const logger = appLogger.child({ route: "/api/debug/cancel-subscription" });
|
const logger = appLogger.child({ route: "/api/debug/cancel-subscription" });
|
||||||
|
|
||||||
|
29
app/api/debug/get-payments.ts
Normal file
29
app/api/debug/get-payments.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import type { BlitzApiHandler } from "blitz";
|
||||||
|
|
||||||
|
import { getPayments } from "integrations/paddle";
|
||||||
|
import appLogger from "integrations/logger";
|
||||||
|
import db from "db";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ route: "/api/debug/cancel-subscription" });
|
||||||
|
|
||||||
|
const cancelSubscriptionHandler: BlitzApiHandler = async (req, res) => {
|
||||||
|
const { organizationId } = req.body;
|
||||||
|
|
||||||
|
logger.debug(`fetching payments for organizationId="${organizationId}"`);
|
||||||
|
const subscriptions = await db.subscription.findMany({ where: { organizationId } });
|
||||||
|
if (subscriptions.length === 0) {
|
||||||
|
res.status(200).send([]);
|
||||||
|
}
|
||||||
|
console.log("subscriptions", subscriptions);
|
||||||
|
|
||||||
|
const paymentsBySubscription = await Promise.all(
|
||||||
|
subscriptions.map((subscription) => getPayments({ subscriptionId: subscription.paddleSubscriptionId })),
|
||||||
|
);
|
||||||
|
const payments = paymentsBySubscription.flat();
|
||||||
|
const result = Array.from(payments).sort((a, b) => b.payout_date.localeCompare(a.payout_date));
|
||||||
|
logger.debug(result);
|
||||||
|
|
||||||
|
res.status(200).send(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cancelSubscriptionHandler;
|
18
app/api/debug/get-subscription.ts
Normal file
18
app/api/debug/get-subscription.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { BlitzApiHandler } from "blitz";
|
||||||
|
|
||||||
|
import db from "db";
|
||||||
|
import appLogger from "integrations/logger";
|
||||||
|
|
||||||
|
const logger = appLogger.child({ route: "/api/debug/get-subscription" });
|
||||||
|
|
||||||
|
const cancelSubscriptionHandler: BlitzApiHandler = async (req, res) => {
|
||||||
|
const { organizationId } = req.body;
|
||||||
|
|
||||||
|
logger.debug(`fetching subscription for organizationId="${organizationId}"`);
|
||||||
|
const subscription = await db.subscription.findFirst({ where: { organizationId } });
|
||||||
|
console.debug(subscription);
|
||||||
|
|
||||||
|
res.status(200).send(subscription);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default cancelSubscriptionHandler;
|
@ -30,34 +30,27 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
|
|||||||
|
|
||||||
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;
|
||||||
const paddleCheckoutId = event.checkoutId;
|
await db.organization.update({
|
||||||
const paddleSubscriptionId = event.subscriptionId;
|
where: { id: organizationId },
|
||||||
const planId = event.productId;
|
data: {
|
||||||
const nextBillDate = event.nextPaymentDate;
|
subscription: {
|
||||||
const status = translateSubscriptionStatus(event.status);
|
create: {
|
||||||
const lastEventTime = event.eventTime;
|
paddleSubscriptionId: event.subscriptionId,
|
||||||
const updateUrl = event.updateUrl;
|
paddlePlanId: event.productId,
|
||||||
const cancelUrl = event.cancelUrl;
|
paddleCheckoutId: event.checkoutId,
|
||||||
const currency = event.currency;
|
nextBillDate: event.nextPaymentDate,
|
||||||
const unitPrice = event.unitPrice;
|
status: translateSubscriptionStatus(event.status),
|
||||||
|
lastEventTime: event.eventTime,
|
||||||
|
updateUrl: event.updateUrl,
|
||||||
|
cancelUrl: event.cancelUrl,
|
||||||
|
currency: event.currency,
|
||||||
|
unitPrice: event.unitPrice,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (!!organization.subscription) {
|
if (!!organization.subscription) {
|
||||||
await db.subscription.update({
|
|
||||||
where: { paddleSubscriptionId: organization.subscription.paddleSubscriptionId },
|
|
||||||
data: {
|
|
||||||
paddleSubscriptionId,
|
|
||||||
paddlePlanId: planId,
|
|
||||||
paddleCheckoutId,
|
|
||||||
nextBillDate,
|
|
||||||
status,
|
|
||||||
lastEventTime,
|
|
||||||
updateUrl,
|
|
||||||
cancelUrl,
|
|
||||||
currency,
|
|
||||||
unitPrice,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
sendEmail({
|
sendEmail({
|
||||||
subject: "Welcome back to Shellphone",
|
subject: "Welcome back to Shellphone",
|
||||||
body: "Welcome back to Shellphone",
|
body: "Welcome back to Shellphone",
|
||||||
@ -66,26 +59,6 @@ export const subscriptionCreatedQueue = Queue<Payload>("api/queue/subscription-c
|
|||||||
logger.error(error);
|
logger.error(error);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await db.organization.update({
|
|
||||||
where: { id: organizationId },
|
|
||||||
data: {
|
|
||||||
subscription: {
|
|
||||||
create: {
|
|
||||||
paddleSubscriptionId,
|
|
||||||
paddlePlanId: planId,
|
|
||||||
paddleCheckoutId,
|
|
||||||
nextBillDate,
|
|
||||||
status,
|
|
||||||
lastEventTime,
|
|
||||||
updateUrl,
|
|
||||||
cancelUrl,
|
|
||||||
currency,
|
|
||||||
unitPrice,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
sendEmail({
|
sendEmail({
|
||||||
subject: "Welcome to Shellphone",
|
subject: "Welcome to Shellphone",
|
||||||
body: `Welcome to Shellphone`,
|
body: `Welcome to Shellphone`,
|
||||||
|
@ -3,76 +3,78 @@ import useSubscription from "../../hooks/use-subscription";
|
|||||||
export default function BillingHistory() {
|
export default function BillingHistory() {
|
||||||
const { payments } = useSubscription();
|
const { payments } = useSubscription();
|
||||||
|
|
||||||
|
if (payments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section className="bg-white pt-6 shadow sm:rounded-md sm:overflow-hidden">
|
||||||
<div className="bg-white pt-6 shadow sm:rounded-md sm:overflow-hidden">
|
<div className="px-4 sm:px-6">
|
||||||
<div className="px-4 sm:px-6">
|
<h2 className="text-lg leading-6 font-medium text-gray-900">Billing history</h2>
|
||||||
<h2 className="text-lg leading-6 font-medium text-gray-900">Billing history</h2>
|
</div>
|
||||||
</div>
|
<div className="mt-6 flex flex-col">
|
||||||
<div className="mt-6 flex flex-col">
|
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
<div className="overflow-hidden border-t border-gray-200">
|
||||||
<div className="overflow-hidden border-t border-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<thead className="bg-gray-50">
|
||||||
<thead className="bg-gray-50">
|
<tr>
|
||||||
<tr>
|
<th
|
||||||
<th
|
scope="col"
|
||||||
scope="col"
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
>
|
||||||
>
|
Date
|
||||||
Date
|
</th>
|
||||||
</th>
|
<th
|
||||||
<th
|
scope="col"
|
||||||
scope="col"
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
>
|
||||||
>
|
Amount
|
||||||
Amount
|
</th>
|
||||||
</th>
|
<th
|
||||||
<th
|
scope="col"
|
||||||
scope="col"
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
>
|
||||||
>
|
Status
|
||||||
Status
|
</th>
|
||||||
</th>
|
<th
|
||||||
<th
|
scope="col"
|
||||||
scope="col"
|
className="relative px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||||
className="relative px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
>
|
||||||
>
|
<span className="sr-only">View receipt</span>
|
||||||
<span className="sr-only">View receipt</span>
|
</th>
|
||||||
</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
{typeof payments !== "undefined"
|
||||||
{typeof payments !== "undefined"
|
? payments.map((payment) => (
|
||||||
? payments.map((payment) => (
|
<tr key={payment.id}>
|
||||||
<tr key={payment.id}>
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<time>{new Date(payment.payout_date).toDateString()}</time>
|
||||||
<time>{new Date(payment.payout_date).toDateString()}</time>
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
{payment.amount} {payment.currency}
|
||||||
{payment.amount} {payment.currency}
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
{payment.is_paid === 1 ? "Paid" : "Upcoming"}
|
||||||
{payment.is_paid === 1 ? "Paid" : "Upcoming"}
|
</td>
|
||||||
</td>
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
{typeof payment.receipt_url !== "undefined" ? (
|
||||||
{typeof payment.receipt_url !== "undefined" ? (
|
<a
|
||||||
<a
|
href={payment.receipt_url}
|
||||||
href={payment.receipt_url}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
className="text-primary-600 hover:text-primary-900"
|
||||||
className="text-primary-600 hover:text-primary-900"
|
>
|
||||||
>
|
View receipt
|
||||||
View receipt
|
</a>
|
||||||
</a>
|
) : null}
|
||||||
) : null}
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
))
|
||||||
))
|
: null}
|
||||||
: null}
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,37 +31,36 @@ const Billing: BlitzPage<Props> = (props) => {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
useRequireOnboarding();
|
useRequireOnboarding();
|
||||||
const { subscription, cancelSubscription, updatePaymentMethod } = useSubscription({
|
const { subscription, cancelSubscription, updatePaymentMethod, payments } = useSubscription({
|
||||||
initialData: props.subscription,
|
initialData: props.subscription,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!subscription) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Plans />
|
|
||||||
<p className="text-sm text-gray-500">Prices include all applicable sales taxes.</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSection>
|
{subscription ? (
|
||||||
<PaddleLink
|
<SettingsSection>
|
||||||
onClick={() => updatePaymentMethod({ updateUrl: subscription.updateUrl })}
|
<p>Current plan: {subscription.paddlePlanId}</p>
|
||||||
text="Update payment method"
|
|
||||||
/>
|
|
||||||
<PaddleLink
|
|
||||||
onClick={() => cancelSubscription({ cancelUrl: subscription.cancelUrl })}
|
|
||||||
text="Cancel subscription"
|
|
||||||
/>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<BillingHistory />
|
<PaddleLink
|
||||||
|
onClick={() => updatePaymentMethod({ updateUrl: subscription.updateUrl })}
|
||||||
|
text="Update payment method"
|
||||||
|
/>
|
||||||
|
<PaddleLink
|
||||||
|
onClick={() => cancelSubscription({ cancelUrl: subscription.cancelUrl })}
|
||||||
|
text="Cancel subscription"
|
||||||
|
/>
|
||||||
|
</SettingsSection>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="hidden lg:block lg:py-3">
|
{payments.length > 0 ? (
|
||||||
<Divider />
|
<>
|
||||||
</div>
|
<BillingHistory />
|
||||||
|
|
||||||
|
<div className="hidden lg:block lg:py-3">
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Plans />
|
<Plans />
|
||||||
<p className="text-sm text-gray-500">Prices include all applicable sales taxes.</p>
|
<p className="text-sm text-gray-500">Prices include all applicable sales taxes.</p>
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { resolver } from "blitz";
|
import { resolver } from "blitz";
|
||||||
|
|
||||||
import db from "db";
|
import db, { SubscriptionStatus } from "db";
|
||||||
|
|
||||||
export default resolver.pipe(resolver.authorize(), async (_ = null, { session }) => {
|
export default resolver.pipe(resolver.authorize(), async (_ = null, { session }) => {
|
||||||
if (!session.orgId) return null;
|
if (!session.orgId) return null;
|
||||||
|
|
||||||
return db.subscription.findFirst({ where: { organizationId: session.orgId } });
|
return db.subscription.findFirst({
|
||||||
|
where: {
|
||||||
|
organizationId: session.orgId,
|
||||||
|
status: { not: SubscriptionStatus.deleted },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user