list payments

This commit is contained in:
m5r 2021-10-01 00:59:35 +02:00
parent 5172ab11e7
commit c5f135fdcc
6 changed files with 110 additions and 59 deletions

View File

@ -67,6 +67,8 @@ export default async function webhook(req: BlitzApiRequest, res: BlitzApiRespons
}
const alertName = req.body.alert_name;
logger.info(`Received ${alertName} webhook`);
logger.info(req.body);
if (isSupportedWebhook(alertName)) {
return handlers[alertName](req, res);
}

View File

@ -1,28 +1,8 @@
const payments = [
{
id: 1,
date: new Date(),
description: "",
amount: "340 USD",
href: "",
},
{
id: 1,
date: new Date(),
description: "",
amount: "340 USD",
href: "",
},
{
id: 1,
date: new Date(),
description: "",
amount: "340 USD",
href: "",
},
];
import useSubscription from "../../hooks/use-subscription";
export default function BillingHistory() {
const { payments } = useSubscription();
return (
<section>
<div className="bg-white pt-6 shadow sm:rounded-md sm:overflow-hidden">
@ -46,17 +26,14 @@ export default function BillingHistory() {
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Description
Amount
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Amount
Status
</th>
{/*
`relative` is added here due to a weird bug in Safari that causes `sr-only` headings to introduce overflow on the body on mobile.
*/}
<th
scope="col"
className="relative px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
@ -66,27 +43,33 @@ export default function BillingHistory() {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{payments.map((payment) => (
<tr key={payment.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<time>{payment.date.toDateString()}</time>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{payment.description}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{payment.amount}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a
href={payment.href}
className="text-primary-600 hover:text-primary-900"
>
View receipt
</a>
</td>
</tr>
))}
{typeof payments !== "undefined"
? payments.map((payment) => (
<tr key={payment.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<time>{new Date(payment.payout_date).toDateString()}</time>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{payment.amount} {payment.currency}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{payment.is_paid === 1 ? "Paid" : "Not paid yet"}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{typeof payment.receipt_url !== "undefined" ? (
<a
href={payment.receipt_url}
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 hover:text-primary-900"
>
View receipt
</a>
) : null}
</td>
</tr>
))
: null}
</tbody>
</table>
</div>

View File

@ -3,6 +3,7 @@ import { useQuery, useMutation, useRouter, useSession } from "blitz";
import type { Subscription } from "db";
import getSubscription from "../queries/get-subscription";
import getPayments from "../queries/get-payments";
import usePaddle from "./use-paddle";
import useCurrentUser from "../../core/hooks/use-current-user";
import updateSubscription from "../mutations/update-subscription";
@ -14,7 +15,8 @@ type Params = {
export default function useSubscription({ initialData }: Params = {}) {
const session = useSession();
const { user } = useCurrentUser();
const [subscription] = useQuery(getSubscription, null, { enabled: Boolean(session.orgId), initialData });
const [subscription] = useQuery(getSubscription, null, { initialData });
const [payments] = useQuery(getPayments, null);
const [updateSubscriptionMutation] = useMutation(updateSubscription);
const router = useRouter();
@ -49,7 +51,7 @@ export default function useSubscription({ initialData }: Params = {}) {
email: user.email,
product: planId,
allowQuantity: false,
passthrough: JSON.stringify({ orgId: session.orgId }),
passthrough: JSON.stringify({ organizationId: session.orgId }),
coupon: "",
};
@ -93,6 +95,7 @@ export default function useSubscription({ initialData }: Params = {}) {
return {
subscription,
payments,
subscribe,
updatePaymentMethod,
cancelSubscription,

View File

@ -0,0 +1,20 @@
import { resolver } from "blitz";
import db, { SubscriptionStatus } from "db";
import { getPayments } from "integrations/paddle";
export default resolver.pipe(resolver.authorize(), async (_ = null, { session }) => {
if (!session.orgId) {
return [];
}
const subscription = await db.subscription.findFirst({
where: { organizationId: session.orgId, status: SubscriptionStatus.active },
});
if (!subscription) {
return [];
}
const payments = await getPayments({ subscriptionId: subscription.paddleSubscriptionId });
return payments.sort((a, b) => b.payout_date.localeCompare(a.payout_date));
});

View File

@ -1,9 +1,9 @@
import type { Ctx } from "blitz";
import { resolver } from "blitz";
import db from "db";
export default async function getCurrentUser(_ = null, { session }: Ctx) {
export default resolver.pipe(resolver.authorize(), async (_ = null, { session }) => {
if (!session.orgId) return null;
return db.subscription.findFirst({ where: { organizationId: session.orgId } });
}
});

View File

@ -12,12 +12,55 @@ const client = got.extend({
async function request<T>(path: string, data: any) {
return client.post<T>(path, {
...data,
vendor_id,
vendor_auth_code,
json: {
...data,
vendor_id,
vendor_auth_code,
},
responseType: "json",
});
}
type GetPaymentsParams = {
subscriptionId: string;
};
export async function getPayments({ subscriptionId }: GetPaymentsParams) {
type Payment = {
id: number;
subscription_id: number;
amount: number;
currency: string;
payout_date: string;
is_paid: number;
is_one_off_charge: boolean;
receipt_url?: string;
};
type PaymentsSuccessResponse = {
success: true;
response: Payment[];
};
type PaymentsErrorResponse = {
success: false;
error: {
code: number;
message: string;
};
};
type PaymentsResponse = PaymentsSuccessResponse | PaymentsErrorResponse;
const { body } = await request<PaymentsResponse>("subscription/payments", { subscription_id: subscriptionId });
console.log("body", typeof body);
if (!body.success) {
throw new Error(body.error.message);
}
return body.response;
}
type UpdateSubscriptionPlanParams = {
subscriptionId: string;
planId: string;
@ -25,7 +68,7 @@ type UpdateSubscriptionPlanParams = {
};
export async function updateSubscriptionPlan({ subscriptionId, planId, prorate = true }: UpdateSubscriptionPlanParams) {
const { body } = await request("/subscription/users/update", {
const { body } = await request("subscription/users/update", {
subscription_id: subscriptionId,
plan_id: planId,
prorate,
@ -35,7 +78,7 @@ export async function updateSubscriptionPlan({ subscriptionId, planId, prorate =
}
export async function cancelPaddleSubscription({ subscriptionId }: { subscriptionId: string }) {
const { body } = await request("/subscription/users_cancel", { subscription_id: subscriptionId });
const { body } = await request("subscription/users_cancel", { subscription_id: subscriptionId });
return body;
}