Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions server/src/internal/balances/balancesRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hono } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { handleCheck } from "../api/check/handleCheck.js";
import { handleCreateBalance } from "./handlers/handleCreateBalance.js";
import { handleDeleteBalance } from "./handlers/handleDeleteBalance.js";
import { handleListBalances } from "./handlers/handleListBalances.js";
import { handleTrack } from "./handlers/handleTrack.js";
import { handleUpdateBalance } from "./handlers/handleUpdateBalance.js";
Expand All @@ -13,6 +14,7 @@ export const balancesRouter = new Hono<HonoEnv>();
balancesRouter.post("/balances/create", ...handleCreateBalance);
balancesRouter.get("/balances/list", ...handleListBalances);
balancesRouter.post("/balances/update", ...handleUpdateBalance);
balancesRouter.post("/balances/delete", ...handleDeleteBalance);

// Track
balancesRouter.post("/events", ...handleTrack);
Expand All @@ -28,5 +30,6 @@ balancesRouter.post("/usage", ...handleSetUsage);
export const balancesRpcRouter = new Hono<HonoEnv>();
balancesRpcRouter.post("/balances.create", ...handleCreateBalance);
balancesRpcRouter.post("/balances.update", ...handleUpdateBalance);
balancesRpcRouter.post("/balances.delete", ...handleDeleteBalance);
balancesRpcRouter.post("/balances.track", ...handleTrack);
balancesRpcRouter.post("/balances.check", ...handleCheck);
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ export const prepareNewBalanceForInsertion = async ({
expires_at: params.expires_at ?? null,
});

// Set external_id if balance_id provided
if (params.balance_id) {
newCustomerEntitlement.external_id = params.balance_id;
}

// If entity is provided, assign balance to entity instead of customer-level
if (entity) {
newCustomerEntitlement.internal_entity_id = entity.internal_id;
Expand Down
74 changes: 74 additions & 0 deletions server/src/internal/balances/handlers/handleDeleteBalance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
ErrCode,
RecaseError,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { z } from "zod/v4";
import { createRoute } from "@/honoMiddlewares/routeHandler";
import { CusService } from "@/internal/customers/CusService";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService";
import { fullCustomerToCustomerEntitlements } from "@autumn/shared";
import { deleteCachedApiCustomer } from "@/internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer";

const DeleteBalanceParamsSchema = z.object({
customer_id: z.string(),
feature_id: z.string(),
balance_id: z.string().optional(),
entity_id: z.string().optional(),
});

export const handleDeleteBalance = createRoute({
body: DeleteBalanceParamsSchema,
handler: async (c) => {
const ctx = c.get("ctx");
const params = c.req.valid("json");
const { customer_id, feature_id, balance_id, entity_id } = params;

const fullCustomer = await CusService.getFull({
ctx,
idOrInternalId: customer_id,
entityId: entity_id,
withEntities: true,
});

const cusEnts = fullCustomerToCustomerEntitlements({
fullCustomer,
featureId: feature_id,
entity: fullCustomer.entity,
customerEntitlementFilters: balance_id
? { externalId: balance_id }
: undefined,
});

if (cusEnts.length === 0) {
throw new RecaseError({
message: `No balances found matching the provided filters`,
code: ErrCode.NotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}

// Mark as expired rather than deleting
const now = Date.now();
for (const cusEnt of cusEnts) {
await CusEntService.update({
ctx,
id: cusEnt.id,
updates: {
expires_at: now,
},
});
Comment on lines +51 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple customer entitlements share the same external_id, all will be expired - verify this is the intended behavior

Prompt To Fix With AI
This is a comment left during a code review.
Path: server/src/internal/balances/handlers/handleDeleteBalance.ts
Line: 51-60

Comment:
If multiple customer entitlements share the same `external_id`, all will be expired - verify this is the intended behavior

How can I resolve this? If you propose a fix, please make it concise.

}

await deleteCachedApiCustomer({
ctx,
customerId: customer_id,
source: `handleDeleteBalance`,
});

return c.json({
success: true,
deleted_count: cusEnts.length,
});
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ export const buildCustomerEntitlementFilters = ({
}: {
params: UpdateBalanceParamsV0;
}): CustomerEntitlementFilters | undefined => {
const { customer_entitlement_id: cusEntId, interval } = params;
const { customer_entitlement_id: cusEntId, interval, balance_id } = params;

const customerEntitlementFilters: CustomerEntitlementFilters | undefined =
cusEntId || interval
cusEntId || interval || balance_id
? {
cusEntIds: cusEntId ? [cusEntId] : undefined,
interval: interval
? resetIntvToEntIntv({ resetIntv: interval })
: undefined,
externalId: balance_id,
}
: undefined;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const getBooleanApiBalance = ({
}): ApiBalanceV1 => {
const feature = cusEnts[0].entitlement.feature;
const planId = cusEntsToPlanId({ cusEnts });
const id = cusEnts[0].id;
const id = cusEnts[0].external_id ?? cusEnts[0].id;

return {
object: "balance",
Expand Down Expand Up @@ -63,7 +63,7 @@ export const getUnlimitedApiBalance = ({
}): ApiBalanceV1 => {
const feature = cusEnts[0].entitlement.feature;
const planId = cusEntsToPlanId({ cusEnts });
const id = cusEnts[0].id;
const id = cusEnts[0].external_id ?? cusEnts[0].id;
const entityId = undefined; // Unlimited features don't have entity context

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const getApiBalanceBreakdownItem = ({
return {
object: "balance_breakdown",

id: customerEntitlement.id,
id: customerEntitlement.external_id ?? customerEntitlement.id,
plan_id: planId,

included_grant: includedGrant,
Expand Down
4 changes: 4 additions & 0 deletions shared/api/balances/create/createBalanceParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { z } from "zod/v4";
import { BalanceParamsBaseSchema } from "../common/balanceParamsBase";

export const ExtCreateBalanceParamsSchema = BalanceParamsBaseSchema.extend({
balance_id: z.string().optional().meta({
description:
"An optional external ID for this balance. Can be used later to target this specific balance in update/delete operations.",
}),
included: z.number().optional().meta({
description:
"The initial balance amount to grant. For metered features, this is the number of units the customer can use.",
Expand Down
4 changes: 4 additions & 0 deletions shared/api/balances/update/updateBalanceParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { z } from "zod/v4";
import { BalanceParamsBaseSchema } from "../common/balanceParamsBase";

export const ExtUpdateBalanceParamsV0Schema = BalanceParamsBaseSchema.extend({
balance_id: z.string().optional().meta({
description:
"Target a specific balance by its external ID. Use when the customer has multiple balances for the same feature.",
}),
remaining: z.number().optional().meta({
description:
"Set the remaining balance to this exact value. Cannot be combined with add_to_balance.",
Expand Down
4 changes: 4 additions & 0 deletions shared/models/cusProductModels/cusEntModels/cusEntModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RolloverSchema } from "./rolloverModels/rolloverTable.js";
export const CustomerEntitlementFiltersSchema = z.object({
cusEntIds: z.array(z.string()).optional(),
interval: z.enum(EntInterval).optional(),
externalId: z.string().optional(),
});

export const EntityBalanceSchema = z.object({
Expand Down Expand Up @@ -44,6 +45,9 @@ export const CustomerEntitlementSchema = z.object({
expires_at: z.number().nullable(),
cache_version: z.number().optional().default(0),

// External ID for API consumers to reference this balance
external_id: z.string().nullish(),

// Group by fields
entities: z.record(z.string(), EntityBalanceSchema).nullish(),
});
Expand Down
3 changes: 3 additions & 0 deletions shared/models/cusProductModels/cusEntModels/cusEntTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export const customerEntitlements = pgTable(
// Optional...
customer_id: text("customer_id"),
feature_id: text("feature_id"),

// External ID for API consumers to reference this balance
external_id: text("external_id"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a database index on external_id since it's used for filtering in update/delete operations

Suggested change
external_id: text("external_id"),
// External ID for API consumers to reference this balance
external_id: text("external_id").index("idx_customer_entitlements_external_id"),
Prompt To Fix With AI
This is a comment left during a code review.
Path: shared/models/cusProductModels/cusEntModels/cusEntTable.ts
Line: 52

Comment:
Consider adding a database index on `external_id` since it's used for filtering in update/delete operations

```suggestion
		// External ID for API consumers to reference this balance
		external_id: text("external_id").index("idx_customer_entitlements_external_id"),
```

How can I resolve this? If you propose a fix, please make it concise.

},
(table) => [
foreignKey({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,12 @@ export const fullCustomerToCustomerEntitlements = ({
);
}

if (notNullish(customerEntitlementFilters?.externalId)) {
cusEnts = cusEnts.filter(
(cusEnt) =>
cusEnt.external_id === customerEntitlementFilters.externalId,
);
}

return cusEnts as FullCusEntWithFullCusProduct[];
};
Loading