fix: track negative values cap at granted balance
This commit is contained in:
@@ -21,3 +21,4 @@ BUN_PARALLEL_COMPACT \
|
|||||||
'server/tests/balances/track/allocated' \
|
'server/tests/balances/track/allocated' \
|
||||||
'server/tests/balances/track/entity-balances' \
|
'server/tests/balances/track/entity-balances' \
|
||||||
'server/tests/balances/track/concurrency' \
|
'server/tests/balances/track/concurrency' \
|
||||||
|
'server/tests/balances/track/negative' \
|
||||||
|
|||||||
@@ -197,68 +197,82 @@ local function deductFromCurrentBalance(cusFeature, amount, adjustGrantedBalance
|
|||||||
local breakdownCurrentBalance = breakdown.current_balance or 0
|
local breakdownCurrentBalance = breakdown.current_balance or 0
|
||||||
-- For refunds (negative amount), always apply. For deductions, only if balance > 0
|
-- For refunds (negative amount), always apply. For deductions, only if balance > 0
|
||||||
if remaining < 0 or breakdownCurrentBalance > 0 then
|
if remaining < 0 or breakdownCurrentBalance > 0 then
|
||||||
-- Calculate how much we can deduct (ensure current_balance never goes below 0)
|
local toDeduct
|
||||||
local maxDeductible = breakdownCurrentBalance
|
if remaining < 0 then
|
||||||
local toDeduct = math.min(remaining, maxDeductible)
|
-- Refund/Negative track: Cap at granted_balance
|
||||||
|
-- We want to add (-remaining) to current_balance
|
||||||
-- Collect Redis deltas
|
-- But we can add at most (granted_balance - current_balance)
|
||||||
table.insert(deltas, {key = breakdown._key, field = "current_balance", delta = -toDeduct})
|
local grantedBalance = breakdown.granted_balance or 0
|
||||||
table.insert(deltas, {key = cusFeature._key, field = "current_balance", delta = -toDeduct})
|
local maxAddable = math.max(0, grantedBalance - breakdownCurrentBalance)
|
||||||
|
local toAdd = math.min(-remaining, maxAddable)
|
||||||
-- Either increment usage or decrement granted_balance based on flag
|
toDeduct = -toAdd
|
||||||
if adjustGrantedBalance then
|
|
||||||
table.insert(deltas, {key = breakdown._key, field = "granted_balance", delta = -toDeduct})
|
|
||||||
table.insert(deltas, {key = cusFeature._key, field = "granted_balance", delta = -toDeduct})
|
|
||||||
else
|
else
|
||||||
table.insert(deltas, {key = breakdown._key, field = "usage", delta = toDeduct})
|
-- Deduction: Cap at current_balance
|
||||||
table.insert(deltas, {key = cusFeature._key, field = "usage", delta = toDeduct})
|
-- Calculate how much we can deduct (ensure current_balance never goes below 0)
|
||||||
|
local maxDeductible = breakdownCurrentBalance
|
||||||
|
toDeduct = math.min(remaining, maxDeductible)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Collect state changes
|
if toDeduct ~= 0 then
|
||||||
local newBalance = breakdownCurrentBalance - toDeduct
|
-- Collect Redis deltas
|
||||||
-- Ensure current_balance never goes below 0
|
table.insert(deltas, {key = breakdown._key, field = "current_balance", delta = -toDeduct})
|
||||||
if newBalance < 0 then
|
table.insert(deltas, {key = cusFeature._key, field = "current_balance", delta = -toDeduct})
|
||||||
newBalance = 0
|
|
||||||
end
|
-- Either increment usage or decrement granted_balance based on flag
|
||||||
|
if adjustGrantedBalance then
|
||||||
table.insert(stateChanges, {
|
table.insert(deltas, {key = breakdown._key, field = "granted_balance", delta = -toDeduct})
|
||||||
type = "breakdown",
|
table.insert(deltas, {key = cusFeature._key, field = "granted_balance", delta = -toDeduct})
|
||||||
index = index,
|
else
|
||||||
field = "current_balance",
|
table.insert(deltas, {key = breakdown._key, field = "usage", delta = toDeduct})
|
||||||
newValue = newBalance
|
table.insert(deltas, {key = cusFeature._key, field = "usage", delta = toDeduct})
|
||||||
})
|
end
|
||||||
if adjustGrantedBalance then
|
|
||||||
|
-- Collect state changes
|
||||||
|
local newBalance = breakdownCurrentBalance - toDeduct
|
||||||
|
-- Ensure current_balance never goes below 0
|
||||||
|
if newBalance < 0 then
|
||||||
|
newBalance = 0
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(stateChanges, {
|
table.insert(stateChanges, {
|
||||||
type = "breakdown",
|
type = "breakdown",
|
||||||
index = index,
|
index = index,
|
||||||
field = "granted_balance",
|
field = "current_balance",
|
||||||
delta = -toDeduct
|
newValue = newBalance
|
||||||
})
|
})
|
||||||
|
if adjustGrantedBalance then
|
||||||
|
table.insert(stateChanges, {
|
||||||
|
type = "breakdown",
|
||||||
|
index = index,
|
||||||
|
field = "granted_balance",
|
||||||
|
delta = -toDeduct
|
||||||
|
})
|
||||||
|
table.insert(stateChanges, {
|
||||||
|
type = "cusFeature",
|
||||||
|
field = "granted_balance",
|
||||||
|
delta = -toDeduct
|
||||||
|
})
|
||||||
|
else
|
||||||
|
table.insert(stateChanges, {
|
||||||
|
type = "breakdown",
|
||||||
|
index = index,
|
||||||
|
field = "usage",
|
||||||
|
delta = toDeduct
|
||||||
|
})
|
||||||
|
table.insert(stateChanges, {
|
||||||
|
type = "cusFeature",
|
||||||
|
field = "usage",
|
||||||
|
delta = toDeduct
|
||||||
|
})
|
||||||
|
end
|
||||||
table.insert(stateChanges, {
|
table.insert(stateChanges, {
|
||||||
type = "cusFeature",
|
type = "cusFeature",
|
||||||
field = "granted_balance",
|
field = "current_balance",
|
||||||
delta = -toDeduct
|
delta = -toDeduct
|
||||||
})
|
})
|
||||||
else
|
|
||||||
table.insert(stateChanges, {
|
remaining = remaining - toDeduct
|
||||||
type = "breakdown",
|
|
||||||
index = index,
|
|
||||||
field = "usage",
|
|
||||||
delta = toDeduct
|
|
||||||
})
|
|
||||||
table.insert(stateChanges, {
|
|
||||||
type = "cusFeature",
|
|
||||||
field = "usage",
|
|
||||||
delta = toDeduct
|
|
||||||
})
|
|
||||||
end
|
end
|
||||||
table.insert(stateChanges, {
|
|
||||||
type = "cusFeature",
|
|
||||||
field = "current_balance",
|
|
||||||
delta = -toDeduct
|
|
||||||
})
|
|
||||||
|
|
||||||
remaining = remaining - toDeduct
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -266,47 +280,59 @@ local function deductFromCurrentBalance(cusFeature, amount, adjustGrantedBalance
|
|||||||
local topLevelCurrentBalance = cusFeature.current_balance or 0
|
local topLevelCurrentBalance = cusFeature.current_balance or 0
|
||||||
-- For refunds (negative amount), always apply. For deductions, only if balance > 0
|
-- For refunds (negative amount), always apply. For deductions, only if balance > 0
|
||||||
if remaining < 0 or topLevelCurrentBalance > 0 then
|
if remaining < 0 or topLevelCurrentBalance > 0 then
|
||||||
-- Calculate how much we can deduct (ensure current_balance never goes below 0)
|
local toDeduct
|
||||||
local maxDeductible = topLevelCurrentBalance
|
if remaining < 0 then
|
||||||
local toDeduct = math.min(remaining, maxDeductible)
|
-- Refund/Negative track: Cap at granted_balance
|
||||||
|
local grantedBalance = cusFeature.granted_balance or 0
|
||||||
-- Collect Redis deltas
|
local maxAddable = math.max(0, grantedBalance - topLevelCurrentBalance)
|
||||||
table.insert(deltas, {key = cusFeature._key, field = "current_balance", delta = -toDeduct})
|
local toAdd = math.min(-remaining, maxAddable)
|
||||||
|
toDeduct = -toAdd
|
||||||
-- Either increment usage or decrement granted_balance based on flag
|
|
||||||
if adjustGrantedBalance then
|
|
||||||
table.insert(deltas, {key = cusFeature._key, field = "granted_balance", delta = -toDeduct})
|
|
||||||
else
|
else
|
||||||
table.insert(deltas, {key = cusFeature._key, field = "usage", delta = toDeduct})
|
-- Deduction: Cap at current_balance
|
||||||
|
-- Calculate how much we can deduct (ensure current_balance never goes below 0)
|
||||||
|
local maxDeductible = topLevelCurrentBalance
|
||||||
|
toDeduct = math.min(remaining, maxDeductible)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Collect state changes
|
if toDeduct ~= 0 then
|
||||||
local newBalance = topLevelCurrentBalance - toDeduct
|
-- Collect Redis deltas
|
||||||
-- Ensure current_balance never goes below 0
|
table.insert(deltas, {key = cusFeature._key, field = "current_balance", delta = -toDeduct})
|
||||||
if newBalance < 0 then
|
|
||||||
newBalance = 0
|
-- Either increment usage or decrement granted_balance based on flag
|
||||||
end
|
if adjustGrantedBalance then
|
||||||
|
table.insert(deltas, {key = cusFeature._key, field = "granted_balance", delta = -toDeduct})
|
||||||
table.insert(stateChanges, {
|
else
|
||||||
type = "cusFeature",
|
table.insert(deltas, {key = cusFeature._key, field = "usage", delta = toDeduct})
|
||||||
field = "current_balance",
|
end
|
||||||
newValue = newBalance
|
|
||||||
})
|
-- Collect state changes
|
||||||
if adjustGrantedBalance then
|
local newBalance = topLevelCurrentBalance - toDeduct
|
||||||
|
-- Ensure current_balance never goes below 0
|
||||||
|
if newBalance < 0 then
|
||||||
|
newBalance = 0
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(stateChanges, {
|
table.insert(stateChanges, {
|
||||||
type = "cusFeature",
|
type = "cusFeature",
|
||||||
field = "granted_balance",
|
field = "current_balance",
|
||||||
delta = -toDeduct
|
newValue = newBalance
|
||||||
})
|
|
||||||
else
|
|
||||||
table.insert(stateChanges, {
|
|
||||||
type = "cusFeature",
|
|
||||||
field = "usage",
|
|
||||||
delta = toDeduct
|
|
||||||
})
|
})
|
||||||
|
if adjustGrantedBalance then
|
||||||
|
table.insert(stateChanges, {
|
||||||
|
type = "cusFeature",
|
||||||
|
field = "granted_balance",
|
||||||
|
delta = -toDeduct
|
||||||
|
})
|
||||||
|
else
|
||||||
|
table.insert(stateChanges, {
|
||||||
|
type = "cusFeature",
|
||||||
|
field = "usage",
|
||||||
|
delta = toDeduct
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
remaining = remaining - toDeduct
|
||||||
end
|
end
|
||||||
|
|
||||||
remaining = remaining - toDeduct
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1107,7 +1133,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Step 3: Check if request can succeed based on overage behavior
|
-- Step 3: Check if request can succeed based on overage behavior
|
||||||
if remainingAmount ~= 0 and overageBehavior == "reject" then
|
if remainingAmount > 0 and overageBehavior == "reject" then
|
||||||
return {
|
return {
|
||||||
success = false,
|
success = false,
|
||||||
error = "INSUFFICIENT_BALANCE"
|
error = "INSUFFICIENT_BALANCE"
|
||||||
|
|||||||
21
server/src/external/sentry/sentryUtils.ts
vendored
Normal file
21
server/src/external/sentry/sentryUtils.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as Sentry from "@sentry/bun";
|
||||||
|
import type { AutumnContext } from "../../honoUtils/HonoEnv";
|
||||||
|
|
||||||
|
export const setSentryTags = ({
|
||||||
|
ctx,
|
||||||
|
customerId,
|
||||||
|
messageId,
|
||||||
|
}: {
|
||||||
|
ctx: AutumnContext;
|
||||||
|
customerId?: string;
|
||||||
|
messageId?: string;
|
||||||
|
}) => {
|
||||||
|
Sentry.setTags({
|
||||||
|
org_id: ctx.org.id,
|
||||||
|
org_slug: ctx.org.slug,
|
||||||
|
env: ctx.env,
|
||||||
|
request_id: ctx.id,
|
||||||
|
customer_id: customerId,
|
||||||
|
message_id: messageId,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Sentry from "@sentry/bun";
|
|
||||||
import type { Context, Next } from "hono";
|
import type { Context, Next } from "hono";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { setSentryTags } from "../external/sentry/sentryUtils";
|
||||||
|
|
||||||
export const parseCustomerIdFromUrl = ({
|
export const parseCustomerIdFromUrl = ({
|
||||||
url,
|
url,
|
||||||
@@ -137,9 +137,9 @@ export const analyticsMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
|||||||
|
|
||||||
ctx.logger.info(`${method} ${c.req.path} (${ctx.org?.slug})`);
|
ctx.logger.info(`${method} ${c.req.path} (${ctx.org?.slug})`);
|
||||||
|
|
||||||
Sentry.setUser({
|
setSentryTags({
|
||||||
...reqContext,
|
ctx,
|
||||||
body: undefined,
|
customerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Execute the request
|
// Execute the request
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const handleCheck = createRoute({
|
|||||||
handler: async (c) => {
|
handler: async (c) => {
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
customer_id,
|
customer_id,
|
||||||
feature_id,
|
feature_id,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ErrCode, type EventInsert, events } from "@autumn/shared";
|
import { ErrCode, type EventInsert, events, RecaseError } from "@autumn/shared";
|
||||||
import { and, desc, eq } from "drizzle-orm";
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
|
||||||
|
|
||||||
export class EventService {
|
export class EventService {
|
||||||
static async insert({ db, event }: { db: DrizzleCli; event: EventInsert }) {
|
static async insert({ db, event }: { db: DrizzleCli; event: EventInsert }) {
|
||||||
@@ -12,24 +11,14 @@ export class EventService {
|
|||||||
.values(event as any)
|
.values(event as any)
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (results.length === 0) {
|
return results?.[0];
|
||||||
throw new RecaseError({
|
|
||||||
message: "Failed to insert event",
|
|
||||||
code: ErrCode.CreateEventFailed,
|
|
||||||
data: results,
|
|
||||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results[0];
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === "23505") {
|
if (error.code === "23505") {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message:
|
message:
|
||||||
"Event (event_name, customer_id, idempotency_key) already exists.",
|
"Event (event_name, customer_id, idempotency_key) already exists.",
|
||||||
code: ErrCode.DuplicateEvent,
|
code: ErrCode.DuplicateEvent,
|
||||||
// data: error,
|
statusCode: StatusCodes.CONFLICT,
|
||||||
statusCode: StatusCodes.BAD_REQUEST,
|
|
||||||
});
|
});
|
||||||
} else throw error;
|
} else throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,26 +123,6 @@ export const runRedisDeduction = async ({
|
|||||||
}: RunRedisDeductionParams): Promise<RunRedisDeductionResult> => {
|
}: RunRedisDeductionParams): Promise<RunRedisDeductionResult> => {
|
||||||
const { org, env, skipCache } = ctx;
|
const { org, env, skipCache } = ctx;
|
||||||
|
|
||||||
// const hasContUseFeature = featureDeductions.some((deduction) =>
|
|
||||||
// isContUseFeature({ feature: deduction.feature }),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 1. Check for idempotency key
|
|
||||||
if (trackParams.idempotency_key) {
|
|
||||||
return {
|
|
||||||
fallback: true,
|
|
||||||
code: "idempotency_key",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// // 2. Check for continuous use feature
|
|
||||||
// if (hasContUseFeature) {
|
|
||||||
// return {
|
|
||||||
// fallback: true,
|
|
||||||
// code: "allocated_feature",
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (query.skip_cache || skipCache) {
|
if (query.skip_cache || skipCache) {
|
||||||
return {
|
return {
|
||||||
fallback: true,
|
fallback: true,
|
||||||
@@ -150,22 +130,22 @@ export const runRedisDeduction = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
customer_id: customerId,
|
||||||
|
customer_data: customerData,
|
||||||
|
entity_id: entityId,
|
||||||
|
entity_data: entityData,
|
||||||
|
} = trackParams;
|
||||||
|
|
||||||
|
const { apiCustomer } = await getOrCreateApiCustomer({
|
||||||
|
ctx,
|
||||||
|
customerId,
|
||||||
|
customerData,
|
||||||
|
entityId,
|
||||||
|
entityData,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await tryRedisWrite<RunRedisDeductionResult>(async () => {
|
const result = await tryRedisWrite<RunRedisDeductionResult>(async () => {
|
||||||
const {
|
|
||||||
customer_id: customerId,
|
|
||||||
customer_data: customerData,
|
|
||||||
entity_id: entityId,
|
|
||||||
entity_data: entityData,
|
|
||||||
} = trackParams;
|
|
||||||
|
|
||||||
const { apiCustomer } = await getOrCreateApiCustomer({
|
|
||||||
ctx,
|
|
||||||
customerId,
|
|
||||||
customerData,
|
|
||||||
entityId,
|
|
||||||
entityData,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Map feature deductions to the format expected by batching manager
|
// Map feature deductions to the format expected by batching manager
|
||||||
const mappedDeductions = featureDeductions.map(
|
const mappedDeductions = featureDeductions.map(
|
||||||
({ feature, deduction }) => ({
|
({ feature, deduction }) => ({
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import {
|
|||||||
type TrackParams,
|
type TrackParams,
|
||||||
type TrackResponseV2,
|
type TrackResponseV2,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
import { db } from "../../../db/initDrizzle";
|
||||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
|
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
|
||||||
|
import { EventService } from "../../api/events/EventService";
|
||||||
|
import { CusService } from "../../customers/CusService";
|
||||||
import { runRedisDeduction } from "./redisTrackUtils/runRedisDeduction";
|
import { runRedisDeduction } from "./redisTrackUtils/runRedisDeduction";
|
||||||
|
import { constructEvent, type EventInfo } from "./trackUtils/eventUtils";
|
||||||
import { executePostgresTracking } from "./trackUtils/executePostgresTracking";
|
import { executePostgresTracking } from "./trackUtils/executePostgresTracking";
|
||||||
import type { FeatureDeduction } from "./trackUtils/getFeatureDeductions";
|
import type { FeatureDeduction } from "./trackUtils/getFeatureDeductions";
|
||||||
|
|
||||||
@@ -32,6 +35,41 @@ export const runTrack = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const eventInfo: EventInfo = {
|
||||||
|
event_name: body.feature_id || body.event_name || "",
|
||||||
|
value: body.value ?? 1,
|
||||||
|
properties: body.properties,
|
||||||
|
timestamp: body.timestamp,
|
||||||
|
idempotency_key: body.idempotency_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If idempotency key is provided, insert event first
|
||||||
|
if (body.idempotency_key) {
|
||||||
|
const customer = await CusService.getFull({
|
||||||
|
db,
|
||||||
|
idOrInternalId: body.customer_id,
|
||||||
|
orgId: ctx.org.id,
|
||||||
|
env: ctx.env,
|
||||||
|
entityId: body.entity_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newEvent = constructEvent({
|
||||||
|
ctx,
|
||||||
|
eventInfo,
|
||||||
|
internalCustomerId: customer?.internal_id ?? "",
|
||||||
|
internalEntityId: customer?.entity?.internal_id ?? undefined,
|
||||||
|
customerId: body.customer_id,
|
||||||
|
entityId: body.entity_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.insert({
|
||||||
|
db,
|
||||||
|
event: newEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
body.skip_event = true;
|
||||||
|
}
|
||||||
|
|
||||||
const { fallback, balances } = await runRedisDeduction({
|
const { fallback, balances } = await runRedisDeduction({
|
||||||
ctx,
|
ctx,
|
||||||
query: {
|
query: {
|
||||||
@@ -41,13 +79,7 @@ export const runTrack = async ({
|
|||||||
trackParams: body,
|
trackParams: body,
|
||||||
featureDeductions,
|
featureDeductions,
|
||||||
overageBehavior: body.overage_behavior || "cap",
|
overageBehavior: body.overage_behavior || "cap",
|
||||||
eventInfo: {
|
eventInfo,
|
||||||
event_name: body.feature_id || body.event_name || "",
|
|
||||||
value: body.value ?? 1,
|
|
||||||
properties: body.properties,
|
|
||||||
timestamp: body.timestamp,
|
|
||||||
idempotency_key: body.idempotency_key,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let response: TrackResponseV2;
|
let response: TrackResponseV2;
|
||||||
|
|||||||
@@ -76,13 +76,15 @@ export const executePostgresTracking = async ({
|
|||||||
entityId: body.entity_id,
|
entityId: body.entity_id,
|
||||||
deductions: featureDeductions,
|
deductions: featureDeductions,
|
||||||
overageBehaviour: body.overage_behavior,
|
overageBehaviour: body.overage_behavior,
|
||||||
eventInfo: {
|
eventInfo: body.idempotency_key
|
||||||
event_name: body.feature_id || body.event_name || "",
|
? undefined
|
||||||
value: body.value ?? 1,
|
: {
|
||||||
properties: body.properties,
|
event_name: body.feature_id || body.event_name || "",
|
||||||
timestamp: body.timestamp,
|
value: body.value ?? 1,
|
||||||
idempotency_key: body.idempotency_key,
|
properties: body.properties,
|
||||||
},
|
timestamp: body.timestamp,
|
||||||
|
idempotency_key: body.idempotency_key,
|
||||||
|
},
|
||||||
refreshCache: true,
|
refreshCache: true,
|
||||||
fullCus,
|
fullCus,
|
||||||
skipAdditionalBalance: true,
|
skipAdditionalBalance: true,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { handleListCustomers } from "./handlers/handleListCustomers.js";
|
|||||||
import { handlePostCustomer } from "./handlers/handlePostCustomerV2.js";
|
import { handlePostCustomer } from "./handlers/handlePostCustomerV2.js";
|
||||||
import { handleTransferProductV2 } from "./handlers/handleTransferProductV2.js";
|
import { handleTransferProductV2 } from "./handlers/handleTransferProductV2.js";
|
||||||
import { handleUpdateBalancesV2 } from "./handlers/handleUpdateBalancesV2.js";
|
import { handleUpdateBalancesV2 } from "./handlers/handleUpdateBalancesV2.js";
|
||||||
|
import { handleUpdateCusEntitlementV2 } from "./handlers/handleUpdateCusEntitlementV2.js";
|
||||||
import { handleUpdateCustomerV2 } from "./handlers/handleUpdateCustomerV2.js";
|
import { handleUpdateCustomerV2 } from "./handlers/handleUpdateCustomerV2.js";
|
||||||
|
|
||||||
export const expressCusRouter = express.Router();
|
export const expressCusRouter = express.Router();
|
||||||
@@ -33,3 +34,7 @@ cusRouter.post("/:customer_id/billing_portal", ...handleCreateBillingPortal);
|
|||||||
|
|
||||||
// Legacy...
|
// Legacy...
|
||||||
cusRouter.post("/:customer_id/balances", ...handleUpdateBalancesV2);
|
cusRouter.post("/:customer_id/balances", ...handleUpdateBalancesV2);
|
||||||
|
cusRouter.post(
|
||||||
|
"/:customer_id/entitlements/:customer_entitlement_id",
|
||||||
|
...handleUpdateCusEntitlementV2,
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { InternalError, notNullish } from "@autumn/shared";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { createRoute } from "../../../honoMiddlewares/routeHandler";
|
||||||
|
import { runDeductionTx } from "../../balances/track/trackUtils/runDeductionTx";
|
||||||
|
import { CusService } from "../CusService";
|
||||||
|
import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService";
|
||||||
|
import { deleteCachedApiCustomer } from "../cusUtils/apiCusCacheUtils/deleteCachedApiCustomer";
|
||||||
|
|
||||||
|
export const handleUpdateCusEntitlementV2 = createRoute({
|
||||||
|
body: z.object({
|
||||||
|
balance: z.number(),
|
||||||
|
next_reset_at: z.number().nullish(),
|
||||||
|
entity_id: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
handler: async (c) => {
|
||||||
|
const ctx = c.get("ctx");
|
||||||
|
const { customer_id, customer_entitlement_id } = c.req.param();
|
||||||
|
const { balance, next_reset_at, entity_id } = c.req.valid("json");
|
||||||
|
const { db, org, env } = ctx;
|
||||||
|
|
||||||
|
const fullCus = await CusService.getFull({
|
||||||
|
db,
|
||||||
|
idOrInternalId: customer_id,
|
||||||
|
orgId: org.id,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cusEnt = fullCus.customer_products
|
||||||
|
.flatMap((cp) => cp.customer_entitlements)
|
||||||
|
.find((ce) => ce.id === customer_entitlement_id);
|
||||||
|
if (!cusEnt) {
|
||||||
|
throw new InternalError({
|
||||||
|
message: `[update cus entitlement] Customer entitlement not found: ${customer_entitlement_id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await runDeductionTx({
|
||||||
|
ctx,
|
||||||
|
customerId: customer_id,
|
||||||
|
entityId: entity_id ?? undefined,
|
||||||
|
deductions: [
|
||||||
|
{
|
||||||
|
feature: cusEnt.entitlement.feature,
|
||||||
|
deduction: 0,
|
||||||
|
targetBalance: balance,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skipAdditionalBalance: true,
|
||||||
|
alterGrantedBalance: true,
|
||||||
|
sortParams: {
|
||||||
|
cusEntId: customer_entitlement_id,
|
||||||
|
},
|
||||||
|
refreshCache: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notNullish(next_reset_at) && next_reset_at !== cusEnt.next_reset_at) {
|
||||||
|
await CusEntService.update({
|
||||||
|
db,
|
||||||
|
id: customer_entitlement_id,
|
||||||
|
updates: {
|
||||||
|
next_reset_at,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteCachedApiCustomer({
|
||||||
|
orgId: ctx.org.id,
|
||||||
|
env: ctx.env,
|
||||||
|
customerId: customer_id,
|
||||||
|
source: "handleUpdateBalance",
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json({ success: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -47,7 +47,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => {
|
|||||||
const { customer_entitlement_id } = req.params;
|
const { customer_entitlement_id } = req.params;
|
||||||
const { balance, next_reset_at, entity_id } = req.body;
|
const { balance, next_reset_at, entity_id } = req.body;
|
||||||
|
|
||||||
if (isNaN(parseFloat(balance))) {
|
if (Number.isNaN(parseFloat(balance))) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: "Invalid balance",
|
message: "Invalid balance",
|
||||||
code: ErrCode.InvalidRequest,
|
code: ErrCode.InvalidRequest,
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import {
|
|||||||
ErrCode,
|
ErrCode,
|
||||||
FeatureNotFoundError,
|
FeatureNotFoundError,
|
||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
|
RecaseError,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { EntityService } from "@/internal/api/entities/EntityService.js";
|
import { EntityService } from "@/internal/api/entities/EntityService.js";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
|
||||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
|
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
|
||||||
import type { ExtendedRequest } from "../../../../utils/models/Request.js";
|
import type { ExtendedRequest } from "../../../../utils/models/Request.js";
|
||||||
import { CusService } from "../../../customers/CusService.js";
|
import { CusService } from "../../../customers/CusService.js";
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/** biome-ignore-all lint/complexity/noStaticOnlyClass: shush */
|
|
||||||
|
|
||||||
import {
|
|
||||||
ErrCode,
|
|
||||||
type IdempotentOperation,
|
|
||||||
type InsertIdempotentOperation,
|
|
||||||
idempotency,
|
|
||||||
RecaseError,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import { sqlNow } from "@shared/db/utils.js";
|
|
||||||
import { StatusCodes } from "http-status-codes";
|
|
||||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
|
||||||
|
|
||||||
export class IdempotencyService {
|
|
||||||
static async create({
|
|
||||||
db,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
db: DrizzleCli;
|
|
||||||
data: InsertIdempotentOperation;
|
|
||||||
}) {
|
|
||||||
return await db
|
|
||||||
.insert(idempotency as any)
|
|
||||||
.values(data)
|
|
||||||
.returning();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async get({
|
|
||||||
db,
|
|
||||||
id,
|
|
||||||
}: {
|
|
||||||
db: DrizzleCli;
|
|
||||||
id: string;
|
|
||||||
}): Promise<IdempotentOperation | null> {
|
|
||||||
const result = await db.query.idempotency.findFirst({
|
|
||||||
where: (idempotency, { eq, gt }) =>
|
|
||||||
eq(idempotency.id, id) && gt(idempotency.expires_at, sqlNow),
|
|
||||||
});
|
|
||||||
return result ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async validate({
|
|
||||||
db,
|
|
||||||
id,
|
|
||||||
}: {
|
|
||||||
db: DrizzleCli;
|
|
||||||
id: string;
|
|
||||||
}): Promise<void> {
|
|
||||||
const idempotency = await IdempotencyService.get({ db, id });
|
|
||||||
if (idempotency === null) return;
|
|
||||||
else {
|
|
||||||
throw new RecaseError({
|
|
||||||
message: "Idempotency key already exists",
|
|
||||||
code: ErrCode.IdempotencyKeyAlreadyExists,
|
|
||||||
statusCode: StatusCodes.CONFLICT,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as Sentry from "@sentry/bun";
|
import { setSentryTags } from "../external/sentry/sentryUtils";
|
||||||
|
|
||||||
const handleResFinish = (req: any, res: any) => {
|
const handleResFinish = (req: any, res: any) => {
|
||||||
const skipUrls = ["/v1/customers/all/search"];
|
const skipUrls = ["/v1/customers/all/search"];
|
||||||
@@ -42,20 +42,22 @@ const parseCustomerIdFromUrl = (url: string): string | undefined => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const analyticsMiddleware = async (req: any, res: any, next: any) => {
|
export const analyticsMiddleware = async (req: any, res: any, next: any) => {
|
||||||
|
const customerId =
|
||||||
|
req?.body?.customer_id || parseCustomerIdFromUrl(req.originalUrl);
|
||||||
|
|
||||||
const reqContext = {
|
const reqContext = {
|
||||||
org_id: req.org?.id,
|
org_id: req.org?.id,
|
||||||
org_slug: req.org?.slug,
|
org_slug: req.org?.slug,
|
||||||
env: req.env,
|
env: req.env,
|
||||||
authType: req.authType,
|
authType: req.authType,
|
||||||
body: req.body,
|
body: req.body,
|
||||||
customer_id:
|
customer_id: customerId,
|
||||||
req?.body?.customer_id || parseCustomerIdFromUrl(req.originalUrl),
|
|
||||||
user_id: req.userId || null,
|
user_id: req.userId || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
Sentry.setUser({
|
setSentryTags({
|
||||||
...reqContext,
|
ctx: req,
|
||||||
body: undefined,
|
customerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (req.span) {
|
if (req.span) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
await import("../sentry.js");
|
await import("../sentry.js");
|
||||||
|
|
||||||
import { AuthType } from "@autumn/shared";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DeleteMessageCommand,
|
DeleteMessageCommand,
|
||||||
type Message,
|
type Message,
|
||||||
@@ -20,6 +18,7 @@ import { runRewardMigrationTask } from "@/internal/migrations/runRewardMigration
|
|||||||
import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js";
|
import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js";
|
||||||
import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js";
|
import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js";
|
||||||
import { generateId } from "@/utils/genUtils.js";
|
import { generateId } from "@/utils/genUtils.js";
|
||||||
|
import { setSentryTags } from "../external/sentry/sentryUtils.js";
|
||||||
import { createWorkerContext } from "./createWorkerContext.js";
|
import { createWorkerContext } from "./createWorkerContext.js";
|
||||||
import { QUEUE_URL, sqs } from "./initSqs.js";
|
import { QUEUE_URL, sqs } from "./initSqs.js";
|
||||||
import { JobName } from "./JobName.js";
|
import { JobName } from "./JobName.js";
|
||||||
@@ -100,12 +99,8 @@ const processMessage = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
Sentry.setUser({
|
setSentryTags({
|
||||||
org_id: ctx.org?.id,
|
ctx,
|
||||||
org_slug: ctx.org?.slug,
|
|
||||||
env: ctx.env,
|
|
||||||
authType: AuthType.Worker,
|
|
||||||
jobName: job.name,
|
|
||||||
messageId: message.MessageId,
|
messageId: message.MessageId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { addHours, addMonths } from "date-fns";
|
import { addHours, addMonths } from "date-fns";
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
|
||||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
|
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
|
||||||
@@ -51,7 +50,6 @@ const premiumProduct = constructProduct({
|
|||||||
const testCase = "downgrade5";
|
const testCase = "downgrade5";
|
||||||
describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => {
|
describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => {
|
||||||
const customerId = testCase;
|
const customerId = testCase;
|
||||||
const autumn: AutumnInt = new AutumnInt();
|
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
let stripeCli: Stripe;
|
let stripeCli: Stripe;
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import {
|
|||||||
ProductItemInterval,
|
ProductItemInterval,
|
||||||
type ProductV2,
|
type ProductV2,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import chalk from "chalk";
|
|
||||||
import { addWeeks } from "date-fns";
|
|
||||||
import { defaultApiVersion } from "@tests/constants.js";
|
import { defaultApiVersion } from "@tests/constants.js";
|
||||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||||
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
|
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
|
||||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { addWeeks } from "date-fns";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { timeout } from "@/utils/genUtils.js";
|
import { timeout } from "@/utils/genUtils.js";
|
||||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
@@ -37,8 +37,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage pro
|
|||||||
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
|
|
||||||
const curUnix = new Date().getTime();
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initProductsV0({
|
await initProductsV0({
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
@@ -45,13 +45,12 @@ const basic = constructRawProduct({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
describe(`${chalk.yellowBright("newVersion4: Testing add ons")}`, () => {
|
||||||
const customerId = "temp";
|
const customerId = "newVersion4";
|
||||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||||
|
|
||||||
let testClockId: string;
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const result = await initCustomerV3({
|
await initCustomerV3({
|
||||||
ctx,
|
ctx,
|
||||||
customerId,
|
customerId,
|
||||||
customerData: {},
|
customerData: {},
|
||||||
@@ -109,7 +108,7 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
|||||||
product_id: pro.id,
|
product_id: pro.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.current_product?.scenario).toBe("renew");
|
expect(res.product?.scenario).toBe("renew");
|
||||||
|
|
||||||
await autumn.attach({
|
await autumn.attach({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { LegacyVersion } from "@autumn/shared";
|
|
||||||
import { beforeAll, describe, expect, test } from "bun:test";
|
import { beforeAll, describe, expect, test } from "bun:test";
|
||||||
import chalk from "chalk";
|
import { LegacyVersion } from "@autumn/shared";
|
||||||
import { addHours, addMonths, addWeeks } from "date-fns";
|
|
||||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
|
||||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||||
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
|
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
|
||||||
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
|
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
|
||||||
import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils.js";
|
import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils.js";
|
||||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||||
|
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { addHours, addMonths, addWeeks } from "date-fns";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { timeout } from "@/utils/genUtils.js";
|
import { timeout } from "@/utils/genUtils.js";
|
||||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const getCustomerEvents = async ({
|
|||||||
customerId: string;
|
customerId: string;
|
||||||
}) => {
|
}) => {
|
||||||
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||||
console.log("Fetching customer with autumn id");
|
|
||||||
const customer = await autumnV2.customers.get(customerId, {
|
const customer = await autumnV2.customers.get(customerId, {
|
||||||
with_autumn_id: true,
|
with_autumn_id: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ describe(`${chalk.yellowBright(`${testCase}: Tracking allocated feature with con
|
|||||||
|
|
||||||
const values = [];
|
const values = [];
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const randomVal =
|
const randomVal = Math.floor(Math.random() * 5);
|
||||||
Math.floor(Math.random() * 5) * (Math.random() < 0.3 ? -1 : 1);
|
// * (Math.random() < 0.3 ? -1 : 1);
|
||||||
promises.push(
|
promises.push(
|
||||||
autumn.track({
|
autumn.track({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
@@ -71,8 +71,17 @@ describe(`${chalk.yellowBright(`${testCase}: Tracking allocated feature with con
|
|||||||
value: randomVal,
|
value: randomVal,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Calculate expected balance with constraint: balance can never exceed includedUsage
|
||||||
|
// (i.e., usage can never go below 0)
|
||||||
|
const potentialBalance = startingBalance - randomVal;
|
||||||
|
const cappedBalance = Math.min(
|
||||||
|
potentialBalance,
|
||||||
|
userItem.included_usage,
|
||||||
|
);
|
||||||
|
|
||||||
totalUsage += randomVal;
|
totalUsage += randomVal;
|
||||||
startingBalance -= randomVal;
|
startingBalance = cappedBalance;
|
||||||
values.push(randomVal);
|
values.push(randomVal);
|
||||||
|
|
||||||
numberOfTracks++;
|
numberOfTracks++;
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing negative values (refunds/cr
|
|||||||
const usage = customer.features[TestFeature.Messages].usage;
|
const usage = customer.features[TestFeature.Messages].usage;
|
||||||
|
|
||||||
// Balance should increase by full 50
|
// Balance should increase by full 50
|
||||||
expect(balance).toBe(130);
|
expect(balance).toBe(100);
|
||||||
// Usage should decrease by 50 (from 20 to -30)
|
// Usage should decrease by 50 (from 20 to -30)
|
||||||
expect(usage).toBe(-30);
|
expect(usage).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reflect large refund in non-cached customer after 2s", async () => {
|
test("should reflect large refund in non-cached customer after 2s", async () => {
|
||||||
@@ -129,7 +129,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing negative values (refunds/cr
|
|||||||
const balance = customer.features[TestFeature.Messages].balance;
|
const balance = customer.features[TestFeature.Messages].balance;
|
||||||
const usage = customer.features[TestFeature.Messages].usage;
|
const usage = customer.features[TestFeature.Messages].usage;
|
||||||
|
|
||||||
expect(balance).toBe(130);
|
expect(balance).toBe(100);
|
||||||
expect(usage).toBe(-30);
|
expect(usage).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { beforeAll, describe, expect, test } from "bun:test";
|
import { beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { ApiVersion } from "@autumn/shared";
|
import { ApiVersion, ErrCode } from "@autumn/shared";
|
||||||
import chalk from "chalk";
|
|
||||||
import { Decimal } from "decimal.js";
|
|
||||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { Decimal } from "decimal.js";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||||
|
import { expectAutumnError } from "../../../utils/expectUtils/expectErrUtils";
|
||||||
|
import { timeout } from "../../../utils/genUtils";
|
||||||
|
import { getCustomerEvents } from "../../testBalanceUtils";
|
||||||
|
|
||||||
const messagesFeature = constructFeatureItem({
|
const messagesFeature = constructFeatureItem({
|
||||||
featureId: TestFeature.Messages,
|
featureId: TestFeature.Messages,
|
||||||
@@ -72,6 +75,11 @@ describe(`${chalk.yellowBright("track-basic6: test idempotency key prevents dupl
|
|||||||
|
|
||||||
expect(balance).toBe(expectedBalance);
|
expect(balance).toBe(expectedBalance);
|
||||||
expect(usage).toBe(deductValue);
|
expect(usage).toBe(deductValue);
|
||||||
|
|
||||||
|
const eventsList = await getCustomerEvents({ customerId });
|
||||||
|
expect(eventsList).toHaveLength(1);
|
||||||
|
expect(eventsList?.[0].idempotency_key).toBe(idempotencyKey);
|
||||||
|
expect(eventsList?.[0].value).toBe(deductValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should reject second track with same idempotency key", async () => {
|
test("should reject second track with same idempotency key", async () => {
|
||||||
@@ -81,28 +89,35 @@ describe(`${chalk.yellowBright("track-basic6: test idempotency key prevents dupl
|
|||||||
// Get balance before attempting duplicate track
|
// Get balance before attempting duplicate track
|
||||||
const customerBefore = await autumnV1.customers.get(customerId);
|
const customerBefore = await autumnV1.customers.get(customerId);
|
||||||
const balanceBefore = customerBefore.features[TestFeature.Messages].balance;
|
const balanceBefore = customerBefore.features[TestFeature.Messages].balance;
|
||||||
|
|
||||||
// This should fail or be rejected due to duplicate idempotency key
|
// This should fail or be rejected due to duplicate idempotency key
|
||||||
let errorThrown = false;
|
await expectAutumnError({
|
||||||
try {
|
errCode: ErrCode.DuplicateEvent,
|
||||||
await autumnV1.track({
|
func: async () => {
|
||||||
customer_id: customerId,
|
await autumnV1.track({
|
||||||
feature_id: TestFeature.Messages,
|
customer_id: customerId,
|
||||||
value: deductValue,
|
feature_id: TestFeature.Messages,
|
||||||
idempotency_key: idempotencyKey,
|
value: deductValue,
|
||||||
});
|
idempotency_key: idempotencyKey,
|
||||||
} catch (error) {
|
});
|
||||||
errorThrown = true;
|
},
|
||||||
// Optionally check error type/message
|
});
|
||||||
}
|
|
||||||
|
|
||||||
expect(errorThrown).toBe(true);
|
|
||||||
|
|
||||||
// Balance should remain unchanged
|
// Balance should remain unchanged
|
||||||
const customerAfter = await autumnV1.customers.get(customerId);
|
const customerAfter = await autumnV1.customers.get(customerId);
|
||||||
const balanceAfter = customerAfter.features[TestFeature.Messages].balance;
|
|
||||||
|
|
||||||
|
const balanceAfter = customerAfter.features[TestFeature.Messages].balance;
|
||||||
expect(balanceAfter).toBe(balanceBefore);
|
expect(balanceAfter).toBe(balanceBefore);
|
||||||
|
|
||||||
|
await timeout(2000);
|
||||||
|
const customerAfter2 = await autumnV1.customers.get(customerId, {
|
||||||
|
skip_cache: "true",
|
||||||
|
});
|
||||||
|
const balanceAfter2 = customerAfter2.features[TestFeature.Messages].balance;
|
||||||
|
expect(balanceAfter2).toBe(balanceBefore);
|
||||||
|
|
||||||
|
const eventsList = await getCustomerEvents({ customerId });
|
||||||
|
expect(eventsList).toHaveLength(1);
|
||||||
|
expect(eventsList?.[0].idempotency_key).toBe(idempotencyKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should process track with different idempotency key", async () => {
|
test("should process track with different idempotency key", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { beforeAll, describe, expect, test } from "bun:test";
|
import { beforeAll, describe, expect, test } from "bun:test";
|
||||||
import { ApiVersion, type TrackResponseV2 } from "@autumn/shared";
|
import {
|
||||||
|
type ApiCustomer,
|
||||||
|
ApiVersion,
|
||||||
|
type TrackResponseV2,
|
||||||
|
} from "@autumn/shared";
|
||||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@@ -8,6 +12,7 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
|||||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||||
|
import { timeout } from "../../../utils/genUtils";
|
||||||
|
|
||||||
const messagesFeature = constructFeatureItem({
|
const messagesFeature = constructFeatureItem({
|
||||||
featureId: TestFeature.Messages,
|
featureId: TestFeature.Messages,
|
||||||
@@ -62,16 +67,46 @@ describe(`${chalk.yellowBright("track-negative1: track negative on meterd featur
|
|||||||
value: deductValue,
|
value: deductValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(trackRes.balance).toBeDefined();
|
expect(trackRes.balance).toMatchObject({
|
||||||
expect(trackRes.balance?.feature_id).toBe(TestFeature.Messages);
|
granted_balance: 100,
|
||||||
expect(trackRes.balance?.current_balance).toBe(100 - deductValue);
|
current_balance: 100,
|
||||||
expect(trackRes.balance?.usage).toBe(deductValue);
|
purchased_balance: 0,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const customer = await autumnV1.customers.get(customerId);
|
test("should deduct first, then deduct negative value and have correct balance", async () => {
|
||||||
const balance = customer.features[TestFeature.Messages].balance;
|
const deductValue1 = 50;
|
||||||
const usage = customer.features[TestFeature.Messages].usage;
|
const deductValue2 = -37.89;
|
||||||
|
|
||||||
expect(balance).toBe(100 - deductValue);
|
await autumnV2.track({
|
||||||
expect(usage).toBe(deductValue);
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Messages,
|
||||||
|
value: deductValue1,
|
||||||
|
});
|
||||||
|
const trackRes2: TrackResponseV2 = await autumnV2.track({
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Messages,
|
||||||
|
value: deductValue2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trackRes2.balance).toMatchObject({
|
||||||
|
granted_balance: 100,
|
||||||
|
current_balance: 100 - deductValue1 - deductValue2,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: deductValue1 + deductValue2,
|
||||||
|
});
|
||||||
|
|
||||||
|
await timeout(2000);
|
||||||
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||||
|
skip_cache: "true",
|
||||||
|
});
|
||||||
|
const balance = customer.balances[TestFeature.Messages];
|
||||||
|
expect(balance).toMatchObject({
|
||||||
|
granted_balance: 100,
|
||||||
|
current_balance: 100 - deductValue1 - deductValue2,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: deductValue1 + deductValue2,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
152
server/tests/balances/track/negative/track-negative3.test.ts
Normal file
152
server/tests/balances/track/negative/track-negative3.test.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { beforeAll, describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
type ApiCustomer,
|
||||||
|
ApiVersion,
|
||||||
|
type TrackResponseV2,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||||
|
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
|
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||||
|
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||||
|
import { timeout } from "../../../utils/genUtils";
|
||||||
|
|
||||||
|
const userItem = constructFeatureItem({
|
||||||
|
featureId: TestFeature.Users,
|
||||||
|
includedUsage: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const freeProd = constructProduct({
|
||||||
|
type: "free",
|
||||||
|
isDefault: false,
|
||||||
|
items: [userItem],
|
||||||
|
});
|
||||||
|
|
||||||
|
const testCase = "track-negative3";
|
||||||
|
|
||||||
|
describe(`${chalk.yellowBright("track-negative3: track negative on free allocated feature, should cap with granted balance")}`, () => {
|
||||||
|
const customerId = "track-negative3";
|
||||||
|
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||||
|
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await initCustomerV3({
|
||||||
|
ctx,
|
||||||
|
customerId,
|
||||||
|
withTestClock: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await initProductsV0({
|
||||||
|
ctx,
|
||||||
|
products: [freeProd],
|
||||||
|
prefix: testCase,
|
||||||
|
});
|
||||||
|
|
||||||
|
await autumnV1.attach({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: freeProd.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should track positive into 'overage' ", async () => {
|
||||||
|
const trackValue = 8;
|
||||||
|
|
||||||
|
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
value: trackValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trackRes.balance).toBeDefined();
|
||||||
|
expect(trackRes.balance).toMatchObject({
|
||||||
|
current_balance: 0,
|
||||||
|
purchased_balance: 3,
|
||||||
|
usage: trackValue,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should track negative and cap at granted balance", async () => {
|
||||||
|
// Currently at -3
|
||||||
|
const trackValue = -20;
|
||||||
|
|
||||||
|
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
value: trackValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trackRes.balance).toMatchObject({
|
||||||
|
granted_balance: 5,
|
||||||
|
current_balance: 5,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-cached customer should reflect changes", async () => {
|
||||||
|
await timeout(2000);
|
||||||
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||||
|
skip_cache: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customer.balances[TestFeature.Users]).toMatchObject({
|
||||||
|
granted_balance: 5,
|
||||||
|
current_balance: 5,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should update balance and cap at granted balance", async () => {
|
||||||
|
await autumnV2.balances.update({
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
current_balance: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||||
|
const balance = customer.balances[TestFeature.Users];
|
||||||
|
|
||||||
|
expect(balance).toMatchObject({
|
||||||
|
granted_balance: 10,
|
||||||
|
current_balance: 10,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await autumnV2.track({
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
value: 5,
|
||||||
|
});
|
||||||
|
const trackRes: TrackResponseV2 = await autumnV2.track({
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
value: -100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trackRes.balance).toMatchObject({
|
||||||
|
granted_balance: 10,
|
||||||
|
current_balance: 10,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("non-cached customer should reflect changes", async () => {
|
||||||
|
await timeout(2000);
|
||||||
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||||
|
skip_cache: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customer.balances[TestFeature.Users]).toMatchObject({
|
||||||
|
granted_balance: 10,
|
||||||
|
current_balance: 10,
|
||||||
|
purchased_balance: 0,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -59,7 +59,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
console.log("DB:", {
|
console.log("DB:", {
|
||||||
bal: cusEnt?.balance,
|
bal: cusEnt?.balance,
|
||||||
add_bal: cusEnt?.additional_balance,
|
add_bal: cusEnt?.additional_balance,
|
||||||
add_grant: cusEnt?.additional_granted_balance,
|
add_grant: cusEnt?.adjustment,
|
||||||
});
|
});
|
||||||
console.log("API:", {
|
console.log("API:", {
|
||||||
granted: balance.granted_balance,
|
granted: balance.granted_balance,
|
||||||
@@ -111,7 +111,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
const cusEnt = await getRawCusEnt();
|
const cusEnt = await getRawCusEnt();
|
||||||
expect(cusEnt?.balance).toBe(0); // Unchanged
|
expect(cusEnt?.balance).toBe(0); // Unchanged
|
||||||
expect(cusEnt?.additional_balance).toBe(10); // Added
|
expect(cusEnt?.additional_balance).toBe(10); // Added
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(10); // Added
|
expect(cusEnt?.adjustment).toBe(10); // Added
|
||||||
|
|
||||||
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||||
const balance = customer.balances[TestFeature.Users];
|
const balance = customer.balances[TestFeature.Users];
|
||||||
@@ -139,7 +139,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
const cusEnt = await getRawCusEnt();
|
const cusEnt = await getRawCusEnt();
|
||||||
expect(cusEnt?.balance).toBe(0); // Unchanged (all came from add_bal)
|
expect(cusEnt?.balance).toBe(0); // Unchanged (all came from add_bal)
|
||||||
expect(cusEnt?.additional_balance).toBe(5); // 10 - 5
|
expect(cusEnt?.additional_balance).toBe(5); // 10 - 5
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(5); // 10 - 5
|
expect(cusEnt?.adjustment).toBe(5); // 10 - 5
|
||||||
|
|
||||||
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||||
const balance = customer.balances[TestFeature.Users];
|
const balance = customer.balances[TestFeature.Users];
|
||||||
@@ -166,7 +166,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
const cusEnt = await getRawCusEnt();
|
const cusEnt = await getRawCusEnt();
|
||||||
expect(cusEnt?.balance).toBe(0);
|
expect(cusEnt?.balance).toBe(0);
|
||||||
expect(cusEnt?.additional_balance).toBe(0); // Floored
|
expect(cusEnt?.additional_balance).toBe(0); // Floored
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(0); // 5 - 5
|
expect(cusEnt?.adjustment).toBe(0); // 5 - 5
|
||||||
|
|
||||||
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||||
const balance = customer.balances[TestFeature.Users];
|
const balance = customer.balances[TestFeature.Users];
|
||||||
@@ -203,7 +203,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
// add_bal = 0 + 10 = 10, add_grant = 0 + 10 = 10, balance = -5
|
// add_bal = 0 + 10 = 10, add_grant = 0 + 10 = 10, balance = -5
|
||||||
expect(cusEnt?.balance).toBe(-5); // Unchanged
|
expect(cusEnt?.balance).toBe(-5); // Unchanged
|
||||||
expect(cusEnt?.additional_balance).toBe(10);
|
expect(cusEnt?.additional_balance).toBe(10);
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(10);
|
expect(cusEnt?.adjustment).toBe(10);
|
||||||
|
|
||||||
// Now remove: update to 3
|
// Now remove: update to 3
|
||||||
// current = Math.max(0, -5) + 10 = 10
|
// current = Math.max(0, -5) + 10 = 10
|
||||||
@@ -222,7 +222,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
cusEnt = await getRawCusEnt();
|
cusEnt = await getRawCusEnt();
|
||||||
expect(cusEnt?.balance).toBe(-5); // Unchanged
|
expect(cusEnt?.balance).toBe(-5); // Unchanged
|
||||||
expect(cusEnt?.additional_balance).toBe(3); // 10 - 7
|
expect(cusEnt?.additional_balance).toBe(3); // 10 - 7
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(3); // 10 - 7
|
expect(cusEnt?.adjustment).toBe(3); // 10 - 7
|
||||||
});
|
});
|
||||||
|
|
||||||
test("CASE D: balances.update from negative to positive preserves paid credits", async () => {
|
test("CASE D: balances.update from negative to positive preserves paid credits", async () => {
|
||||||
@@ -246,7 +246,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
const cusEnt = await getRawCusEnt();
|
const cusEnt = await getRawCusEnt();
|
||||||
expect(cusEnt?.balance).toBe(-5); // Unchanged (still in debt)
|
expect(cusEnt?.balance).toBe(-5); // Unchanged (still in debt)
|
||||||
expect(cusEnt?.additional_balance).toBe(0); // 3 - 3
|
expect(cusEnt?.additional_balance).toBe(0); // 3 - 3
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(0); // 3 - 3
|
expect(cusEnt?.adjustment).toBe(0); // 3 - 3
|
||||||
});
|
});
|
||||||
|
|
||||||
test("CASE E: Track negative to fully return debt", async () => {
|
test("CASE E: Track negative to fully return debt", async () => {
|
||||||
@@ -267,7 +267,7 @@ describe(`${chalk.yellowBright("balances-update3: Balance decoupling tests")}`,
|
|||||||
const cusEnt = await getRawCusEnt();
|
const cusEnt = await getRawCusEnt();
|
||||||
expect(cusEnt?.balance).toBe(0); // -5 + 5 = 0
|
expect(cusEnt?.balance).toBe(0); // -5 + 5 = 0
|
||||||
expect(cusEnt?.additional_balance).toBe(0); // Unchanged
|
expect(cusEnt?.additional_balance).toBe(0); // Unchanged
|
||||||
expect(cusEnt?.additional_granted_balance).toBe(0); // Unchanged
|
expect(cusEnt?.adjustment).toBe(0); // Unchanged
|
||||||
|
|
||||||
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
const customer = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||||
const balance = customer.balances[TestFeature.Users];
|
const balance = customer.balances[TestFeature.Users];
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ export default function App() {
|
|||||||
email: data.user.email ?? "unknown_email",
|
email: data.user.email ?? "unknown_email",
|
||||||
name: data.user.name ?? "unknown_name",
|
name: data.user.name ?? "unknown_name",
|
||||||
id: data.user.id ?? "unknown_user",
|
id: data.user.id ?? "unknown_user",
|
||||||
orgId: data.session.activeOrganizationId ?? "unknown_org",
|
});
|
||||||
|
Sentry.setTags({
|
||||||
|
org_id: data.session.activeOrganizationId ?? "unknown_org",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|||||||
@@ -92,14 +92,23 @@ function UpdateCusEntitlement({
|
|||||||
|
|
||||||
setUpdateLoading(true);
|
setUpdateLoading(true);
|
||||||
try {
|
try {
|
||||||
await axiosInstance.post("/v1/balances/update", {
|
// await axiosInstance.post("/v1/balances/update", {
|
||||||
customer_id: customer.id || customer.internal_id,
|
// customer_id: customer.id || customer.internal_id,
|
||||||
feature_id: feature.id,
|
// feature_id: feature.id,
|
||||||
current_balance: balanceInt,
|
// current_balance: balanceInt,
|
||||||
customer_entitlement_id: cusEnt.id,
|
// customer_entitlement_id: cusEnt.id,
|
||||||
entity_id: entityId ?? undefined,
|
// entity_id: entityId ?? undefined,
|
||||||
// usage: 0,
|
// // usage: 0,
|
||||||
});
|
// });
|
||||||
|
const customerId = customer.id || customer.internal_id;
|
||||||
|
await axiosInstance.post(
|
||||||
|
`/v1/customers/${customerId}/entitlements/${cusEnt.id}`,
|
||||||
|
{
|
||||||
|
balance: balanceInt,
|
||||||
|
next_reset_at: updateFields.next_reset_at,
|
||||||
|
entity_id: entityId ?? undefined,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
toast.success("Entitlement updated successfully");
|
toast.success("Entitlement updated successfully");
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|||||||
Reference in New Issue
Block a user