Skip to content

Commit

Permalink
fix: report usage to stripe error
Browse files Browse the repository at this point in the history
  • Loading branch information
devrsi0n committed Oct 19, 2024
1 parent fb4f402 commit 3720905
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 42 deletions.
2 changes: 1 addition & 1 deletion apps/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build:local": "dotenv -e .env.local -- next build",
"debug": "NODE_OPTIONS='--inspect' next dev",
"dev": "next dev",
"start": "next start",
"start": "dotenv -e .env.prod -- next start",
"start:local": "dotenv -e .env.local -- next start",
"test": "DEBUG_PRINT_LIMIT=999999 jest --silent=false",
"test:coverage": "DEBUG_PRINT_LIMIT=999999 jest --silent=false --coverage"
Expand Down
56 changes: 47 additions & 9 deletions apps/main/src/pages/api/cron/usage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { prisma, stripe } from '@chirpy-dev/trpc';
import { cpDayjs, queryDailyPVUsage } from '@chirpy-dev/utils';
import { cpDayjs, queryPipe, type QueryPipe } from '@chirpy-dev/utils';
import { NextApiRequest, NextApiResponse } from 'next';
import { log as axiomLog } from 'next-axiom';

Expand All @@ -25,12 +25,13 @@ export default async function updateUsage(
plan: {
in: ['PRO', 'ENTERPRISE'],
},
stripeSubscriptionId: {
stripeSubscriptionItemId: {
not: null,
},
},
select: {
stripeSubscriptionId: true,
stripeSubscriptionItemId: true,
billingCycleDay: true,
projects: {
select: {
domain: true,
Expand All @@ -39,34 +40,35 @@ export default async function updateUsage(
},
});
const updateUserUsage = async (user: (typeof users)[number]) => {
const pv = await queryDailyPVUsage({
const pv = await queryPVUsage({
domains: user.projects.map((p) => p.domain),
billingCycleDay: user.billingCycleDay,
});
// To avoid duplicated reports
const idempotencyKey = `${user.stripeSubscriptionId}_${cpDayjs()
const idempotencyKey = `${user.stripeSubscriptionItemId}_${cpDayjs()
.utc()
.format('YYYY-MM-DD')}`;
try {
await stripe.subscriptionItems.createUsageRecord(
user.stripeSubscriptionId!,
user.stripeSubscriptionItemId!,
{
quantity: pv,
action: 'increment',
action: 'set',
},
{
idempotencyKey,
},
);
} catch (error) {
const msg = `Usage report failed for item ID ${
user.stripeSubscriptionId
user.stripeSubscriptionItemId
} with idempotency key ${idempotencyKey}: ${(error as Error).toString()}`;
log.error(msg);
throw new Error(msg);
}
};
try {
await Promise.allSettled(users.map((u) => updateUserUsage(u)));
await Promise.all(users.map((u) => updateUserUsage(u)));
} catch {
res.status(500).end('Create usage record failed');
}
Expand All @@ -79,3 +81,39 @@ export const config = {
bodyParser: false,
},
};

const DATE_FORMAT = 'YYYY-MM-DD';

/**
* Get pageviews usage
*/
export async function queryPVUsage(params: {
domains: string[];
billingCycleDay: number | null;
}): Promise<number> {
const today = cpDayjs().utc();
const billingCycleDay = params.billingCycleDay || 1;

// For today is after billing cycle day, we query usage from this month to next month
let from = today.date(billingCycleDay);
let to = today.date(billingCycleDay).add(1, 'month');

// For today is before billing cycle day, we query usage from last month to this month
if (today.date() < billingCycleDay) {
from = from.subtract(1, 'month');
to = to.subtract(1, 'month');
}
const usage: QueryPipe<{
pageviews: number;
href: string;
indices: number[];
}> = await queryPipe('pv_by_domains', {
date_from: from.format(DATE_FORMAT),
date_to: to.format(DATE_FORMAT),
domains: params.domains.join(','),
});
return usage.data.reduce((acc, { pageviews }) => {
acc += pageviews;
return acc;
}, 0);
}
1 change: 1 addition & 0 deletions apps/main/src/pages/api/stripe/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default async function stripeWebhook(
data: {
plan: plan.type,
stripeSubscriptionId: subscriptionId,
stripeSubscriptionItemId: subscription.items.data[0].id,
},
select: {
id: true,
Expand Down
6 changes: 5 additions & 1 deletion packages/trpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ docker compose up -d
```sh
# 1. prototyping
pnpm prisma db push
# 2. generate migration history when ready

# 2. generate migration history when ready. Will reset the db
pnpm prisma migrate dev --name <new-feature>

# 3. deploy
pnpm dotenv -e .env.prod -- pnpm prisma migrate deploy
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "stripeSubscriptionItemId" TEXT;
13 changes: 8 additions & 5 deletions packages/trpc/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,15 @@ model User {
settings Settings?
plan Plan? @default(HOBBY)
plan Plan? @default(HOBBY)
// Billing cycle start day
billingCycleDay Int?
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
pages Page[]
billingCycleDay Int?
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
// For usage reporting
stripeSubscriptionItemId String?
pages Page[]
}

enum Plan {
Expand Down
26 changes: 0 additions & 26 deletions packages/utils/src/analytics/tinybird.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { cpDayjs } from '../date';

export class QueryError extends Error {
status: number;
constructor(message: string, status: number) {
Expand Down Expand Up @@ -74,30 +72,6 @@ export function queryPipe<P, D = P>(
return client(`/pipes/${name}.json?${searchParams}`);
}

const DATE_FORMAT = 'YYYY-MM-DD';

/**
* Get daily pageviews usage
*/
export async function queryDailyPVUsage(params: {
domains: string[];
}): Promise<number> {
const dateFrom = cpDayjs().subtract(1, 'day');
const usage: QueryPipe<{
pageviews: number;
href: string;
indices: number[];
}> = await queryPipe('pv_by_domains', {
date_from: dateFrom.format(DATE_FORMAT),
date_to: cpDayjs().format(DATE_FORMAT),
domains: params.domains.join(','),
});
return usage.data.reduce((acc, { pageviews }) => {
acc += pageviews;
return acc;
}, 0);
}

export type QuerySQL<T> = {
meta: Meta<T>[];
data: T[];
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ dayjs.extend(relativeTime);

// eslint-disable-next-line unicorn/prefer-export-from
export const cpDayjs = dayjs;
export type Dayjs = dayjs.Dayjs;

0 comments on commit 3720905

Please sign in to comment.