diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts new file mode 100644 index 000000000..551887b54 --- /dev/null +++ b/server/src/external/autumn/autumnCli.ts @@ -0,0 +1,103 @@ +export class Autumn { + private apiKey: string; + public headers: Record; + public baseUrl: string; + + constructor() { + this.apiKey = process.env.AUTUMN_API_KEY || ""; + this.headers = { + Authorization: `Bearer ${this.apiKey}`, + "Content-Type": "application/json", + }; + this.baseUrl = "https://api.useautumn.com/v1"; + } + + async get(path: string) { + const response = await fetch(`${this.baseUrl}${path}`, { + headers: this.headers, + }); + return response.json(); + } + + async post(path: string, body: any) { + const response = await fetch(`${this.baseUrl}${path}`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(body), + }); + return response.json(); + } + + async createCustomer({ + id, + email, + name, + fingerprint, + }: { + id: string; + email: string; + name: string; + fingerprint?: string; + }) { + const data = await this.post("/customers", { + id, + email, + name, + fingerprint, + }); + + return data; + } + + async attach({ + customerId, + productId, + options, + }: { + customerId: string; + productId: string; + options?: any; + }) { + const data = await this.post(`/attach`, { + customer_id: customerId, + product_id: productId, + options, + }); + + return data; + } + + async sendEvent({ + customerId, + eventName, + properties, + }: { + customerId: string; + eventName: string; + properties?: any; + }) { + const data = await this.post(`/events`, { + customer_id: customerId, + event_name: eventName, + properties, + }); + + return data; + } + + async entitled({ + customerId, + featureId, + quantity, + }: { + customerId: string; + featureId: string; + quantity?: number; + }) { + const data = await this.get( + `/entitled?customer_id=${customerId}&feature_id=${featureId}&quantity=${quantity}` + ); + + return data; + } +} diff --git a/server/src/external/autumn/autumnUtils.ts b/server/src/external/autumn/autumnUtils.ts new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/server/src/external/autumn/autumnUtils.ts @@ -0,0 +1 @@ + diff --git a/server/src/internal/api/customers/cusUtils.ts b/server/src/internal/api/customers/cusUtils.ts index acd0916bc..ec344da92 100644 --- a/server/src/internal/api/customers/cusUtils.ts +++ b/server/src/internal/api/customers/cusUtils.ts @@ -26,6 +26,10 @@ export const createNewCustomer = async ({ customer: CreateCustomer; nextResetAt?: number; }) => { + console.log("Creating new customer"); + console.log("Org ID:", orgId); + console.log("Customer data:", customer); + const org = await OrgService.getFullOrg({ sb, orgId, @@ -33,6 +37,9 @@ export const createNewCustomer = async ({ const customerData: Customer = { ...customer, + name: customer.name || "", + email: customer.email || "", + internal_id: generateId("cus"), org_id: orgId, created_at: Date.now(), diff --git a/server/src/internal/api/customers/products/cusProductRouter.ts b/server/src/internal/api/customers/products/cusProductRouter.ts index 56f790db1..c9730ddf5 100644 --- a/server/src/internal/api/customers/products/cusProductRouter.ts +++ b/server/src/internal/api/customers/products/cusProductRouter.ts @@ -32,6 +32,7 @@ import { ProductService } from "@/internal/products/ProductService.js"; import { handleChangeProduct } from "@/internal/customers/change-product/handleChangeProduct.js"; import chalk from "chalk"; import { AttachParams } from "@/internal/customers/products/AttachParams.js"; +import { Autumn } from "@/external/autumn/autumnCli.js"; export const attachRouter = Router(); diff --git a/server/src/internal/api/events/eventRouter.ts b/server/src/internal/api/events/eventRouter.ts index 7baa94d3c..39abacc05 100644 --- a/server/src/internal/api/events/eventRouter.ts +++ b/server/src/internal/api/events/eventRouter.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import { AppEnv, + CreateEventSchema, Customer, ErrCode, Event, @@ -28,33 +29,7 @@ const getEventAndCustomer = async (req: any) => { const orgId = req.orgId; const env = req.env; - let newEvent: Event; let customer: Customer; - try { - // 1. Validate request body - EventSchema.omit({ - id: true, - org_id: true, - env: true, - properties: true, - }).parse(req.body); - newEvent = { - id: generateId("evt"), - org_id: orgId, - env: env, - timestamp: Date.now(), - properties: body.properties || {}, - - event_name: body.event_name, - customer_id: body.customer_id, - }; - } catch (error: any) { - throw new RecaseError({ - message: "Invalid request body -> " + formatZodError(error), - code: ErrCode.InvalidEvent, - statusCode: StatusCodes.BAD_REQUEST, - }); - } // 2. Check if customer ID is valid customer = await CusService.getCustomer({ @@ -64,6 +39,23 @@ const getEventAndCustomer = async (req: any) => { env: env, }); + const parsedEvent = CreateEventSchema.parse(req.body); + + const newEvent: Event = { + ...parsedEvent, + + properties: parsedEvent.properties || {}, + + timestamp: Date.now(), + id: generateId("evt"), + org_id: orgId, + env: env, + internal_customer_id: customer.internal_id, + }; + + // console.log("Customer:", customer); + // console.log("Org ID:", req.orgId); + if (!customer) { customer = await createNewCustomer({ sb: req.sb, @@ -137,26 +129,6 @@ eventsRouter.post("", async (req: any, res: any) => { features: affectedFeatures, event, }); - // await inngest.send({ - // name: "autumn/update-balance", - // data: { - // customer, - // features: affectedFeatures, - // }, - // }); - // await updateBalanceTask.trigger( - // { - // customer, - // features: affectedFeatures, - // }, - // { - // queue: { - // name: "customer", - // concurrencyLimit: 1, - // }, - // concurrencyKey: customer.internal_id, - // } - // ); } else { console.log("No affected features found"); } diff --git a/server/src/internal/customers/invoices/InvoiceService.ts b/server/src/internal/customers/invoices/InvoiceService.ts index 88fa6914d..def897611 100644 --- a/server/src/internal/customers/invoices/InvoiceService.ts +++ b/server/src/internal/customers/invoices/InvoiceService.ts @@ -4,6 +4,8 @@ import Stripe from "stripe"; import { generateId } from "@/utils/genUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { ErrCode } from "@/errors/errCodes.js"; +import { Autumn } from "@/external/autumn/autumnCli.js"; +import { CusService } from "../CusService.js"; export class InvoiceService { static async createInvoice({ @@ -83,28 +85,44 @@ export class InvoiceService { status: status || (stripeInvoice.status as InvoiceStatus | null), }; - // Check if invoice already exists - // TODO: Fix This - const existingInvoice = await this.getInvoiceByStripeId({ - sb, - stripeInvoiceId: stripeInvoice.id, - }); + // // Check if invoice already exists + // // TODO: Fix This + // const existingInvoice = await this.getInvoiceByStripeId({ + // sb, + // stripeInvoiceId: stripeInvoice.id, + // }); - if (existingInvoice) { - console.log("Invoice already exists"); - return; - } + // if (existingInvoice) { + // console.log("Invoice already exists"); + // return; + // } - const { error } = await sb.from("invoices").upsert(invoice, { - onConflict: "stripe_id", - }); + // // const { error } = await sb + // // .from("invoices") + // // .upsert(invoice, { + // // onConflict: "stripe_id", + // // }) + // // .select(); + + const { error } = await sb.from("invoices").insert(invoice); + + // const customer = await CusService.getByInternalId({ + // sb, + // internalId: invoice.internal_customer_id, + // }); + + // const autumn = new Autumn(); + // await autumn.sendEvent({ + // customerId: invoice.internal_customer_id, + // eventName: "monthly_revenue", + // properties: { + // value: stripeInvoice.total / 100, + // }, + // }); if (error) { console.log("Failed to create invoice from stripe", error); - throw new RecaseError({ - code: ErrCode.CreateInvoiceFailed, - message: error.message, - }); + return; } } } diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts index 47cf712fd..10cc5b153 100644 --- a/server/src/internal/orgs/orgRouter.ts +++ b/server/src/internal/orgs/orgRouter.ts @@ -1,5 +1,5 @@ import { ErrCode } from "@/errors/errCodes.js"; -import { createClerkCli, createClerkOrg } from "@/external/clerkUtils.js"; +import { createClerkCli } from "@/external/clerkUtils.js"; import { checkKeyValid, createWebhookEndpoint, @@ -8,12 +8,8 @@ import { encryptData } from "@/utils/encryptUtils.js"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import express from "express"; import Stripe from "stripe"; -import { CusService } from "../customers/CusService.js"; import { OrgService } from "./OrgService.js"; -import { Customer, Organization, Product } from "@autumn/shared"; import { createStripeCli } from "@/external/stripe/utils.js"; -import { Client } from "pg"; -import { ProductService } from "../products/ProductService.js"; import { AppEnv } from "@autumn/shared"; export const orgRouter = express.Router(); diff --git a/server/src/utils/errorUtils.ts b/server/src/utils/errorUtils.ts index 829682d48..2052411bf 100644 --- a/server/src/utils/errorUtils.ts +++ b/server/src/utils/errorUtils.ts @@ -60,7 +60,6 @@ export const handleRequestError = ({ res.status(400).json({ message: formatZodError(error), code: ErrCode.InvalidInputs, - data: formatZodError(error), }); } else { console.log(`Unknown error | ${action}`, error); diff --git a/shared/models/eventModels/eventModels.ts b/shared/models/eventModels/eventModels.ts index 6d1055701..eaa935921 100644 --- a/shared/models/eventModels/eventModels.ts +++ b/shared/models/eventModels/eventModels.ts @@ -1,25 +1,27 @@ import { z } from "zod"; export const EventSchema = z.object({ + // Submitted by the client + customer_id: z.string().nonempty(), + event_name: z.string().nonempty(), + properties: z.record(z.string(), z.any()), + idempotency_key: z.string().nullish(), + // Internal usage id: z.string(), env: z.string(), org_id: z.string(), - - // Submitted by the client - customer_id: z.string().nonempty(), - event_name: z.string().nonempty(), - - // Optional - properties: z.record(z.string(), z.any()), - idempotency_key: z.string().optional(), - timestamp: z.number().optional(), + timestamp: z.number(), + internal_customer_id: z.string(), }); -export const CreateEventSchema = EventSchema.omit({ - id: true, - env: true, - org_id: true, +export const CreateEventSchema = z.object({ + customer_id: z.string().nonempty(), + event_name: z.string().nonempty(), + properties: z.record(z.string(), z.any()).nullish(), + + idempotency_key: z.string().nullish(), }); export type Event = z.infer; +export type CreateEvent = z.infer;