Skip to content

Commit a4b5623

Browse files
authored
feat: usage (#59)
1 parent a5a44b8 commit a4b5623

File tree

10 files changed

+334
-12
lines changed

10 files changed

+334
-12
lines changed

apps/event-system/services/api/routes/internal.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export const internalRoute = () => ({
3434

3535
'v1.early-access.public.create',
3636

37+
'v1.usage.public.create',
38+
'v1.usage.public.get',
39+
3740
],
3841
aliases: {
3942
'POST v3/users/get': 'v3.users.getUserFromToken',
@@ -71,6 +74,10 @@ export const internalRoute = () => ({
7174
// Early Access
7275
'POST v1/early-access/create': 'v1.early-access.public.create',
7376

77+
// Usage
78+
'POST v1/usage/create': 'v1.usage.public.create',
79+
'GET v1/usage/get': 'v1.usage.public.get',
80+
7481
},
7582
cors: {
7683
origin: '*', //corsOk,

apps/event-system/services/stripe/stripe-webhook.service.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,50 @@ export default {
3939
endpointSecret
4040
);
4141

42-
function analyzeSubscription(subscriptionData: Stripe.SubscriptionItem[]): string {
42+
function analyzeSubscription(subscriptionData: Stripe.SubscriptionItem[]): {
43+
key: string,
44+
buildKitIntegrationLimit: number,
45+
buildKitUsageLimit: number,
46+
chatUsageLimit: number,
47+
} {
4348
const priceIds = new Set(subscriptionData.map((item) => item.price.id));
4449

4550
if (priceIds.has(process.env.STRIPE_PRO_PLAN_PRICE_ID)) {
46-
return 'sub::pro';
51+
52+
const price = subscriptionData.find((item) => item.price.id === process.env.STRIPE_PRO_PLAN_PRICE_ID);
53+
const buildKitIntegrationLimit = price?.plan?.metadata?.buildKitIntegrationLimit;
54+
const buildKitUsageLimit = price?.plan?.metadata?.buildKitUsageLimit;
55+
const chatUsageLimit = price?.plan?.metadata?.chatUsageLimit;
56+
57+
return {
58+
key: 'sub::pro',
59+
buildKitIntegrationLimit: parseInt(buildKitIntegrationLimit) || 10,
60+
buildKitUsageLimit: parseInt(buildKitUsageLimit) || 50,
61+
chatUsageLimit: parseInt(chatUsageLimit) || 500,
62+
};
4763
}
4864

4965
if (priceIds.has(process.env.STRIPE_FREE_PLAN_PRICE_ID)) {
50-
return 'sub::free';
66+
const price = subscriptionData.find((item) => item.price.id === process.env.STRIPE_FREE_PLAN_PRICE_ID);
67+
68+
const buildKitIntegrationLimit = price?.plan?.metadata?.buildKitIntegrationLimit;
69+
const buildKitUsageLimit = price?.plan?.metadata?.buildKitUsageLimit;
70+
const chatUsageLimit = price?.plan?.metadata?.chatUsageLimit;
71+
72+
return {
73+
key: 'sub::free',
74+
buildKitIntegrationLimit: parseInt(buildKitIntegrationLimit) || 3,
75+
buildKitUsageLimit: parseInt(buildKitUsageLimit) || 10,
76+
chatUsageLimit: parseInt(chatUsageLimit) || 50,
77+
};
5178
}
5279

53-
return "sub::unknown";
80+
return {
81+
key: 'sub::unknown',
82+
buildKitIntegrationLimit: 3,
83+
buildKitUsageLimit: 10,
84+
chatUsageLimit: 50,
85+
};
5486

5587
}
5688

@@ -59,12 +91,14 @@ export default {
5991
case 'customer.subscription.created':
6092

6193
const subscriptionCreated = event.data.object;
62-
const key = analyzeSubscription(event.data.object?.items?.data);
94+
const { key, buildKitIntegrationLimit, buildKitUsageLimit, chatUsageLimit } = analyzeSubscription(event.data.object?.items?.data);
6395

6496
if (key && subscriptionCreated?.status === 'active') {
6597
const billing = {
6698
throughput: parseInt(process.env.DEFAULT_CLIENT_THROUGHPUT) || 500,
67-
buildKitIntegrationLimit: parseInt(process.env.DEFAULT_CLIENT_BUILDKIT_INTEGRATION_LIMIT) || 3,
99+
buildKitIntegrationLimit,
100+
buildKitUsageLimit,
101+
chatUsageLimit,
68102
provider: 'stripe',
69103
customerId: subscriptionCreated?.customer,
70104
subscription: {
@@ -103,11 +137,18 @@ export default {
103137
case 'customer.subscription.updated':
104138
const subscriptionUpdated = event.data.object;
105139

106-
const subscriptionKey = analyzeSubscription(subscriptionUpdated?.items?.data);
140+
const {
141+
key: subscriptionKey,
142+
buildKitIntegrationLimit: updatedBuildKitIntegrationLimit,
143+
buildKitUsageLimit: updatedBuildKitUsageLimit,
144+
chatUsageLimit: updatedChatUsageLimit
145+
} = analyzeSubscription(subscriptionUpdated?.items?.data);
107146

108147
const billing = {
109148
throughput: parseInt(process.env.DEFAULT_CLIENT_THROUGHPUT) || 500,
110-
buildKitIntegrationLimit: parseInt(process.env.DEFAULT_CLIENT_BUILDKIT_INTEGRATION_LIMIT) || 3,
149+
buildKitIntegrationLimit: updatedBuildKitIntegrationLimit,
150+
buildKitUsageLimit: updatedBuildKitUsageLimit,
151+
chatUsageLimit: updatedChatUsageLimit,
111152
provider: 'stripe',
112153
customerId: subscriptionUpdated?.customer,
113154
subscription: {
@@ -165,7 +206,9 @@ export default {
165206

166207
const updatedBilling = {
167208
throughput: parseInt(process.env.DEFAULT_CLIENT_THROUGHPUT) || 500,
168-
buildKitIntegrationLimit: parseInt(process.env.DEFAULT_CLIENT_BUILDKIT_INTEGRATION_LIMIT) || 3,
209+
buildKitIntegrationLimit: parseInt((subscriptionCreated as any)?.plan?.metadata?.buildKitIntegrationLimit) || 3,
210+
buildKitUsageLimit: parseInt((subscriptionCreated as any)?.plan?.metadata?.buildKitUsageLimit) || 10,
211+
chatUsageLimit: parseInt((subscriptionCreated as any)?.plan?.metadata?.chatUsageLimit) || 50,
169212
provider: 'stripe',
170213
customerId: subscriptionCreated?.customer,
171214
subscription: {
@@ -175,7 +218,6 @@ export default {
175218
valid: true,
176219
},
177220
};
178-
179221
const client = await ctx.broker.call(
180222
'v1.clients.updateBillingByCustomerId',
181223
{
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const createUsageSchema = {
2+
type: {
3+
type: 'string',
4+
enum: ['buildkit', 'chat'],
5+
required: true,
6+
}
7+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { GenericServiceProvider, makeOwnershipFromContextMeta } from "@libs-private/service-logic/services/genericService";
2+
import { ServiceBroker, Context } from "moleculer";
3+
import MongoDBAdapter from 'moleculer-db-adapter-mongo';
4+
import { createUsageSchema } from "./schema/create.schema";
5+
import { ServiceContextMeta } from "@libs-private/data-models/types/genericService";
6+
import { useUsageService } from "@libs-private/service-logic/services/usage/useUsageService";
7+
export default class Usage extends GenericServiceProvider {
8+
public constructor(public broker: ServiceBroker) {
9+
super(broker, {
10+
name: 'usage',
11+
adapter: new MongoDBAdapter(process.env.MONGO_URI),
12+
version: 1,
13+
collection: 'usage',
14+
hooks: {},
15+
publicActions: {
16+
create: [createUsageSchema, createUsage],
17+
get: [{}, getUsage],
18+
}
19+
});
20+
}
21+
}
22+
23+
const createUsage = async (ctx: Context<unknown, ServiceContextMeta>) => {
24+
const { create } = useUsageService(ctx, makeOwnershipFromContextMeta(ctx));
25+
return (await create(ctx.params as {
26+
type: 'buildkit' | 'chat'
27+
})).unwrap();
28+
};
29+
30+
const getUsage = async (ctx: Context<unknown, ServiceContextMeta>) => {
31+
const { get } = useUsageService(ctx, makeOwnershipFromContextMeta(ctx));
32+
return (await get()).unwrap();
33+
};

libs-private/data-models/types/services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const Services = {
1616
PlatformsOauth: 'v1.platforms-oauth',
1717
Onboarding: 'v1.onboarding',
1818
EarlyAccess: 'v1.early-access',
19+
Usage: 'v1.usage',
1920
} as const;
2021

2122
type keys = keyof typeof Services;

libs-private/service-logic/services/onboarding/useOnboardingService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ export const useOnboardingService = (ctx: Context, ownership: Ownership) => {
3838
const billing = {
3939
throughput: parseInt(process.env.DEFAULT_CLIENT_THROUGHPUT) || 500,
4040
customerId: response?.customer?.id,
41-
buildKitIntegrationLimit: parseInt(process.env.DEFAULT_CLIENT_BUILDKIT_INTEGRATION_LIMIT) || 3,
41+
buildKitIntegrationLimit: parseInt((response?.subscription as any)?.plan?.metadata?.buildKitIntegrationLimit) || 3,
42+
buildKitUsageLimit: parseInt((response?.subscription as any)?.plan?.metadata?.buildKitUsageLimit) || 10,
43+
chatUsageLimit: parseInt((response?.subscription as any)?.plan?.metadata?.chatUsageLimit) || 50,
4244
subscription: {
4345
id: response?.subscription?.id,
4446
endDate: response?.subscription?.current_period_end,
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { Context } from "moleculer";
2+
import { Ownership } from "@libs-private/data-models";
3+
import { BResult } from "@event-inc/types";
4+
import { Usage } from "@event-inc/types/usage";
5+
import { resultErr, resultOk } from "@event-inc/utils";
6+
import jwt from 'jsonwebtoken';
7+
import { format } from 'date-fns';
8+
import { useGenericCRUDService } from "../genericCRUD";
9+
import { Services } from "@libs-private/data-models";
10+
11+
const SERVICE_NAME = Services.Usage;
12+
13+
export const useUsageService = (ctx: Context, ownership: Ownership) => {
14+
const { find, findAndUpdate, updateMany } = useGenericCRUDService(ctx, SERVICE_NAME, ownership, {
15+
DISABLE_ADDING_OWNERSHIP_CHECK: true,
16+
});
17+
18+
return {
19+
async create({
20+
type
21+
}: {
22+
type: "buildkit" | "chat"
23+
}): Promise<BResult<Usage, 'service', unknown>> {
24+
try {
25+
const headers = (ctx.meta as any).request.headers;
26+
const secretKey = headers['x-pica-secret'];
27+
const authorization = headers['authorization'];
28+
29+
if (!secretKey) {
30+
return resultErr<'service'>(
31+
false,
32+
'service_4000',
33+
'Secret key is required',
34+
'buildable-core',
35+
false
36+
);
37+
}
38+
39+
if (!authorization) {
40+
return resultErr<'service'>(
41+
false,
42+
'service_4000',
43+
'User is not authenticated',
44+
'buildable-core',
45+
false
46+
);
47+
}
48+
49+
const authToken = authorization.split(' ')[1];
50+
const decoded = jwt.decode(authToken, { complete: true }) as any;
51+
const environment = secretKey.startsWith('sk_live') ? 'live' : 'test';
52+
const clientId = decoded?.payload?.buildableId;
53+
const currentDate = new Date();
54+
55+
const dailyKey = format(currentDate, 'yyyy-MM-dd');
56+
const monthlyKey = format(currentDate, 'yyyy-MM');
57+
const yearlyKey = format(currentDate, 'yyyy');
58+
59+
// First, try to find the existing usage record
60+
const existingUsage = (await find<Usage>({ query: { clientId } })).unwrap();
61+
62+
if (!existingUsage || existingUsage.length === 0) {
63+
// Direct insert without ownership fields
64+
await ctx.broker.call(`${SERVICE_NAME}.insert`, {
65+
entities: [{
66+
clientId,
67+
buildkit: {
68+
live: {},
69+
test: {}
70+
},
71+
chat: {
72+
live: {},
73+
test: {}
74+
}
75+
}]
76+
});
77+
}
78+
79+
// Update the usage counts using $inc
80+
const updatePath = `${type}.${environment}`;
81+
82+
const result = (await findAndUpdate<Usage>({
83+
query: { clientId },
84+
update: {
85+
$set: {}, // Empty $set to ensure MongoDB treats this as an update operation
86+
$inc: {
87+
[`${updatePath}.total`]: 1,
88+
[`${updatePath}.daily.${dailyKey}`]: 1,
89+
[`${updatePath}.monthly.${monthlyKey}`]: 1,
90+
[`${updatePath}.yearly.${yearlyKey}`]: 1
91+
}
92+
},
93+
options: {
94+
returnDocument: 'after'
95+
}
96+
})).unwrap();
97+
98+
return resultOk(result);
99+
100+
} catch (error) {
101+
return resultErr<'service'>(
102+
false,
103+
'service_4000',
104+
'Something went wrong while updating usage',
105+
'buildable-core',
106+
false
107+
);
108+
}
109+
},
110+
111+
async get(): Promise<BResult<Usage, 'service', unknown>> {
112+
try {
113+
114+
const headers = (ctx.meta as any).request.headers;
115+
const secretKey = headers['x-pica-secret'];
116+
const authorization = headers['authorization'];
117+
118+
if (!secretKey) {
119+
return resultErr<'service'>(
120+
false,
121+
'service_4000',
122+
'Secret key is required',
123+
'buildable-core',
124+
false
125+
);
126+
}
127+
128+
if (!authorization) {
129+
return resultErr<'service'>(
130+
false,
131+
'service_4000',
132+
'User is not authenticated',
133+
'buildable-core',
134+
false
135+
);
136+
}
137+
138+
const authToken = authorization.split(' ')[1];
139+
const decoded = jwt.decode(authToken, { complete: true }) as any;
140+
const clientId = decoded?.payload?.buildableId;
141+
142+
const usage = (await find<Usage>({ query: { clientId } })).unwrap();
143+
144+
if (!usage || usage.length === 0) {
145+
return resultOk({
146+
_id: '',
147+
clientId,
148+
createdAt: 0,
149+
buildkit: {
150+
test: {
151+
total: 0,
152+
daily: {},
153+
monthly: {},
154+
yearly: {}
155+
},
156+
live: {
157+
total: 0,
158+
daily: {},
159+
monthly: {},
160+
yearly: {}
161+
}
162+
},
163+
chat: {
164+
test: {
165+
total: 0,
166+
daily: {},
167+
monthly: {},
168+
yearly: {}
169+
},
170+
live: {
171+
total: 0,
172+
daily: {},
173+
monthly: {},
174+
yearly: {}
175+
}
176+
}
177+
})
178+
}
179+
180+
return resultOk(usage[0] as Usage);
181+
182+
} catch (error) {
183+
return resultErr<'service'>(
184+
false,
185+
'service_4000',
186+
'Something went wrong while fetching usage',
187+
'buildable-core',
188+
false
189+
);
190+
}
191+
}
192+
};
193+
};

0 commit comments

Comments
 (0)