Fix routed Redis reset test coverage
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
||||
type ApiCustomer,
|
||||
type ApiCustomerV3,
|
||||
customerEntitlements,
|
||||
type OrgConfig,
|
||||
} from "@autumn/shared";
|
||||
import { resetAndGetCusEnt } from "@tests/balances/track/rollovers/rolloverTestUtils.js";
|
||||
import { findCustomerEntitlement } from "@tests/balances/utils/findCustomerEntitlement.js";
|
||||
@@ -20,7 +19,6 @@ import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db/initDrizzle.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
const setBalanceInDb = async ({
|
||||
cusEntId,
|
||||
@@ -35,33 +33,14 @@ const setBalanceInDb = async ({
|
||||
.where(eq(customerEntitlements.id, cusEntId));
|
||||
};
|
||||
|
||||
const enablePersistFreeOverage = async ({
|
||||
orgId,
|
||||
orgConfig,
|
||||
}: {
|
||||
orgId: string;
|
||||
orgConfig: OrgConfig;
|
||||
}) => {
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId,
|
||||
updates: { config: { ...orgConfig, persist_free_overage: true } },
|
||||
const persistFreeOverageOrg = ({ slug }: { slug: string }) =>
|
||||
s.platform.create({
|
||||
userEmail: `persist-overage-${slug}-${Math.random()
|
||||
.toString(36)
|
||||
.slice(2, 8)}@autumn.test`,
|
||||
configOverrides: { persist_free_overage: true },
|
||||
setupDefaultFeatures: true,
|
||||
});
|
||||
};
|
||||
|
||||
const disablePersistFreeOverage = async ({
|
||||
orgId,
|
||||
orgConfig,
|
||||
}: {
|
||||
orgId: string;
|
||||
orgConfig: OrgConfig;
|
||||
}) => {
|
||||
await OrgService.update({
|
||||
db,
|
||||
orgId,
|
||||
updates: { config: { ...orgConfig, persist_free_overage: false } },
|
||||
});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 1. Lazy reset (DB path)
|
||||
@@ -73,41 +52,33 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (DB): lazy reset deduc
|
||||
|
||||
const { customerId, autumnV2, ctx } = await initScenario({
|
||||
customerId: "persist-ovg-on-db",
|
||||
setup: [s.customer({ testClock: false }), s.products({ list: [free] })],
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "db" }),
|
||||
s.customer({ testClock: false }),
|
||||
s.products({ list: [free] }),
|
||||
],
|
||||
actions: [s.attach({ productId: free.id })],
|
||||
});
|
||||
|
||||
await enablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
|
||||
await setBalanceInDb({ cusEntId: cusEnt!.id, balance: -100 });
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
try {
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
|
||||
await setBalanceInDb({ cusEntId: cusEnt!.id, balance: -100 });
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
const after = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||
skip_cache: "true",
|
||||
});
|
||||
expect(after.balances[TestFeature.Messages].current_balance).toBe(0);
|
||||
expect(after.balances[TestFeature.Messages].usage).toBe(100);
|
||||
} finally {
|
||||
await disablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
});
|
||||
}
|
||||
const after = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||
skip_cache: "true",
|
||||
});
|
||||
expect(after.balances[TestFeature.Messages].current_balance).toBe(0);
|
||||
expect(after.balances[TestFeature.Messages].usage).toBe(100);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -120,59 +91,51 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (cache): lazy reset de
|
||||
|
||||
const { customerId, autumnV2, ctx } = await initScenario({
|
||||
customerId: "persist-ovg-on-cache",
|
||||
setup: [s.customer({ testClock: false }), s.products({ list: [free] })],
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "cache" }),
|
||||
s.customer({ testClock: false }),
|
||||
s.products({ list: [free] }),
|
||||
],
|
||||
actions: [s.attach({ productId: free.id })],
|
||||
});
|
||||
|
||||
await enablePersistFreeOverage({
|
||||
await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
|
||||
await setBalanceInDb({ cusEntId: cusEnt!.id, balance: -50 });
|
||||
await setCachedCusEntField({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
env: ctx.env,
|
||||
customerId,
|
||||
cusEntId: cusEnt!.id,
|
||||
field: "balance",
|
||||
value: -50,
|
||||
});
|
||||
await setCachedSubjectBalanceField({
|
||||
ctx,
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
customerEntitlementId: cusEnt!.id,
|
||||
field: "balance",
|
||||
value: -50,
|
||||
});
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
try {
|
||||
await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
|
||||
await setBalanceInDb({ cusEntId: cusEnt!.id, balance: -50 });
|
||||
await setCachedCusEntField({
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
customerId,
|
||||
cusEntId: cusEnt!.id,
|
||||
field: "balance",
|
||||
value: -50,
|
||||
});
|
||||
await setCachedSubjectBalanceField({
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
customerEntitlementId: cusEnt!.id,
|
||||
field: "balance",
|
||||
value: -50,
|
||||
redisV2: ctx.redisV2,
|
||||
});
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
const after = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||
expect(after.balances[TestFeature.Messages].current_balance).toBe(50);
|
||||
expect(after.balances[TestFeature.Messages].usage).toBe(50);
|
||||
} finally {
|
||||
await disablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
});
|
||||
}
|
||||
const after = await autumnV2.customers.get<ApiCustomer>(customerId);
|
||||
expect(after.balances[TestFeature.Messages].current_balance).toBe(50);
|
||||
expect(after.balances[TestFeature.Messages].usage).toBe(50);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -185,7 +148,11 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (cron): cron reset ded
|
||||
|
||||
const { customerId, ctx, customer } = await initScenario({
|
||||
customerId: "persist-ovg-on-cron",
|
||||
setup: [s.customer({ testClock: false }), s.products({ list: [free] })],
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "cron" }),
|
||||
s.customer({ testClock: false }),
|
||||
s.products({ list: [free] }),
|
||||
],
|
||||
actions: [s.attach({ productId: free.id })],
|
||||
});
|
||||
|
||||
@@ -219,41 +186,33 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (no overage): positive
|
||||
|
||||
const { customerId, autumnV2, ctx } = await initScenario({
|
||||
customerId: "persist-ovg-on-positive",
|
||||
setup: [s.customer({ testClock: false }), s.products({ list: [free] })],
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "positive" }),
|
||||
s.customer({ testClock: false }),
|
||||
s.products({ list: [free] }),
|
||||
],
|
||||
actions: [s.attach({ productId: free.id })],
|
||||
});
|
||||
|
||||
await enablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
|
||||
await setBalanceInDb({ cusEntId: cusEnt!.id, balance: 50 });
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
try {
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
|
||||
await setBalanceInDb({ cusEntId: cusEnt!.id, balance: 50 });
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
const after = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||
skip_cache: "true",
|
||||
});
|
||||
expect(after.balances[TestFeature.Messages].current_balance).toBe(100);
|
||||
expect(after.balances[TestFeature.Messages].usage).toBe(0);
|
||||
} finally {
|
||||
await disablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
});
|
||||
}
|
||||
const after = await autumnV2.customers.get<ApiCustomer>(customerId, {
|
||||
skip_cache: "true",
|
||||
});
|
||||
expect(after.balances[TestFeature.Messages].current_balance).toBe(100);
|
||||
expect(after.balances[TestFeature.Messages].usage).toBe(0);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -270,6 +229,7 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (per-entity): each ent
|
||||
const { customerId, ctx } = await initScenario({
|
||||
customerId: "persist-ovg-on-entity",
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "entity" }),
|
||||
s.customer({ testClock: false }),
|
||||
s.products({ list: [free] }),
|
||||
s.entities({ count: 2, featureId: TestFeature.Users }),
|
||||
@@ -277,56 +237,43 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (per-entity): each ent
|
||||
actions: [s.attach({ productId: free.id })],
|
||||
});
|
||||
|
||||
await enablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
ctx.org.config = { ...ctx.org.config, persist_free_overage: true };
|
||||
expect(cusEnt).toBeDefined();
|
||||
expect(cusEnt!.entities).toBeDefined();
|
||||
|
||||
try {
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
expect(cusEnt!.entities).toBeDefined();
|
||||
const entities = { ...cusEnt!.entities! };
|
||||
const entityIds = Object.keys(entities);
|
||||
expect(entityIds.length).toBe(2);
|
||||
|
||||
const entities = { ...cusEnt!.entities! };
|
||||
const entityIds = Object.keys(entities);
|
||||
expect(entityIds.length).toBe(2);
|
||||
entities[entityIds[0]].balance = -50;
|
||||
entities[entityIds[0]].adjustment = 0;
|
||||
entities[entityIds[1]].balance = 30;
|
||||
entities[entityIds[1]].adjustment = 0;
|
||||
|
||||
entities[entityIds[0]].balance = -50;
|
||||
entities[entityIds[0]].adjustment = 0;
|
||||
entities[entityIds[1]].balance = 30;
|
||||
entities[entityIds[1]].adjustment = 0;
|
||||
await db
|
||||
.update(customerEntitlements)
|
||||
.set({ entities })
|
||||
.where(eq(customerEntitlements.id, cusEnt!.id));
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
await db
|
||||
.update(customerEntitlements)
|
||||
.set({ entities })
|
||||
.where(eq(customerEntitlements.id, cusEnt!.id));
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
const cusEntAfter = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEntAfter).toBeDefined();
|
||||
expect(cusEntAfter!.entities).toBeDefined();
|
||||
|
||||
const cusEntAfter = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEntAfter).toBeDefined();
|
||||
expect(cusEntAfter!.entities).toBeDefined();
|
||||
|
||||
expect(cusEntAfter!.entities![entityIds[0]].balance).toBe(50);
|
||||
expect(cusEntAfter!.entities![entityIds[1]].balance).toBe(100);
|
||||
} finally {
|
||||
await disablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
});
|
||||
}
|
||||
expect(cusEntAfter!.entities![entityIds[0]].balance).toBe(50);
|
||||
expect(cusEntAfter!.entities![entityIds[1]].balance).toBe(100);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -343,6 +290,7 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (per-entity mixed): di
|
||||
const { customerId, ctx } = await initScenario({
|
||||
customerId: "persist-ovg-on-ent-mix",
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "entity-mixed" }),
|
||||
s.customer({ testClock: false }),
|
||||
s.products({ list: [free] }),
|
||||
s.entities({ count: 2, featureId: TestFeature.Users }),
|
||||
@@ -350,56 +298,43 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (per-entity mixed): di
|
||||
actions: [s.attach({ productId: free.id })],
|
||||
});
|
||||
|
||||
await enablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
ctx.org.config = { ...ctx.org.config, persist_free_overage: true };
|
||||
expect(cusEnt).toBeDefined();
|
||||
expect(cusEnt!.entities).toBeDefined();
|
||||
|
||||
try {
|
||||
const cusEnt = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEnt).toBeDefined();
|
||||
expect(cusEnt!.entities).toBeDefined();
|
||||
const entities = { ...cusEnt!.entities! };
|
||||
const entityIds = Object.keys(entities);
|
||||
expect(entityIds.length).toBe(2);
|
||||
|
||||
const entities = { ...cusEnt!.entities! };
|
||||
const entityIds = Object.keys(entities);
|
||||
expect(entityIds.length).toBe(2);
|
||||
entities[entityIds[0]].balance = -30;
|
||||
entities[entityIds[0]].adjustment = 0;
|
||||
entities[entityIds[1]].balance = -200;
|
||||
entities[entityIds[1]].adjustment = 0;
|
||||
|
||||
entities[entityIds[0]].balance = -30;
|
||||
entities[entityIds[0]].adjustment = 0;
|
||||
entities[entityIds[1]].balance = -200;
|
||||
entities[entityIds[1]].adjustment = 0;
|
||||
await db
|
||||
.update(customerEntitlements)
|
||||
.set({ entities })
|
||||
.where(eq(customerEntitlements.id, cusEnt!.id));
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
|
||||
await db
|
||||
.update(customerEntitlements)
|
||||
.set({ entities })
|
||||
.where(eq(customerEntitlements.id, cusEnt!.id));
|
||||
await expireCusEntForReset({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
const cusEntAfter = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEntAfter).toBeDefined();
|
||||
expect(cusEntAfter!.entities).toBeDefined();
|
||||
|
||||
const cusEntAfter = await findCustomerEntitlement({
|
||||
ctx,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
});
|
||||
expect(cusEntAfter).toBeDefined();
|
||||
expect(cusEntAfter!.entities).toBeDefined();
|
||||
|
||||
expect(cusEntAfter!.entities![entityIds[0]].balance).toBe(70);
|
||||
expect(cusEntAfter!.entities![entityIds[1]].balance).toBe(-100);
|
||||
} finally {
|
||||
await disablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
});
|
||||
}
|
||||
expect(cusEntAfter!.entities![entityIds[0]].balance).toBe(70);
|
||||
expect(cusEntAfter!.entities![entityIds[1]].balance).toBe(-100);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -418,6 +353,7 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (prepaid): invoice res
|
||||
const { customerId, autumnV1, ctx, testClockId } = await initScenario({
|
||||
customerId: "persist-ovg-on-prepaid",
|
||||
setup: [
|
||||
persistFreeOverageOrg({ slug: "prepaid" }),
|
||||
s.customer({ paymentMethod: "success" }),
|
||||
s.products({ list: [pro] }),
|
||||
],
|
||||
@@ -430,38 +366,24 @@ test.concurrent(`${chalk.yellowBright("persist overage ON (prepaid): invoice res
|
||||
],
|
||||
});
|
||||
|
||||
// Enable flag BEFORE advancing, so the webhook handler sees it in DB
|
||||
await enablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
// After attach: prepaid balance = 200 (quantity=200, billingUnits=100, so 2*100=200), lifetime = 50
|
||||
// Total messages balance = 200 + 50 = 250
|
||||
// After track 300: deducted 300 from messages
|
||||
// The prepaid cusEnt's balance should be negative (overage)
|
||||
// On advance, handlePrepaidPrices resets prepaid with persistFreeOverage
|
||||
await advanceToNextInvoice({
|
||||
stripeCli: ctx.stripeCli,
|
||||
testClockId: testClockId!,
|
||||
withPause: true,
|
||||
});
|
||||
|
||||
try {
|
||||
// After attach: prepaid balance = 200 (quantity=200, billingUnits=100, so 2*100=200), lifetime = 50
|
||||
// Total messages balance = 200 + 50 = 250
|
||||
// After track 300: deducted 300 from messages
|
||||
// The prepaid cusEnt's balance should be negative (overage)
|
||||
// On advance, handlePrepaidPrices resets prepaid with persistFreeOverage
|
||||
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||
|
||||
await advanceToNextInvoice({
|
||||
stripeCli: ctx.stripeCli,
|
||||
testClockId: testClockId!,
|
||||
withPause: true,
|
||||
});
|
||||
|
||||
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||
|
||||
// Lifetime messages (50) should be untouched by the prepaid reset
|
||||
// Prepaid: was 200, used 300 -> overage of some amount on the prepaid cusEnt
|
||||
// After reset with persist_free_overage: new prepaid balance = 200 - overage
|
||||
// The exact split depends on deduction order, so just verify the feature exists
|
||||
// and the balance is less than the full 250 (200 prepaid + 50 lifetime)
|
||||
expect(customer.features[TestFeature.Messages]).toBeDefined();
|
||||
expect(customer.features[TestFeature.Messages].balance).toBeLessThan(250);
|
||||
} finally {
|
||||
await disablePersistFreeOverage({
|
||||
orgId: ctx.org.id,
|
||||
orgConfig: ctx.org.config,
|
||||
});
|
||||
}
|
||||
// Lifetime messages (50) should be untouched by the prepaid reset
|
||||
// Prepaid: was 200, used 300 -> overage of some amount on the prepaid cusEnt
|
||||
// After reset with persist_free_overage: new prepaid balance = 200 - overage
|
||||
// The exact split depends on deduction order, so just verify the feature exists
|
||||
// and the balance is less than the full 250 (200 prepaid + 50 lifetime)
|
||||
expect(customer.features[TestFeature.Messages]).toBeDefined();
|
||||
expect(customer.features[TestFeature.Messages].balance).toBeLessThan(250);
|
||||
});
|
||||
|
||||
@@ -7,11 +7,26 @@ import { findCustomerEntitlement } from "@tests/balances/utils/findCustomerEntit
|
||||
import type { TestContext } from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Redis } from "ioredis";
|
||||
import { redis } from "@/external/redis/initRedis.js";
|
||||
import { getCtxWithCustomerRedis } from "@/external/redis/customerRedisRouting.js";
|
||||
import { redis, waitForRedisReady } from "@/external/redis/initRedis.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { buildSharedFullSubjectBalanceKey } from "@/internal/customers/cache/fullSubject/builders/buildSharedFullSubjectBalanceKey.js";
|
||||
import { buildFullCustomerCacheKey } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/fullCustomerCacheConfig.js";
|
||||
|
||||
const getRoutedRedisForCustomer = async ({
|
||||
ctx,
|
||||
customerId,
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
customerId: string;
|
||||
}): Promise<Redis> => {
|
||||
const { ctx: routedCtx } = getCtxWithCustomerRedis({ ctx, customerId });
|
||||
await waitForRedisReady(routedCtx.redisV2, "customer-redis", 5000).catch(
|
||||
() => undefined,
|
||||
);
|
||||
return routedCtx.redisV2;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update next_reset_at for a specific cusEnt in the Redis FullCustomer cache.
|
||||
* Reads the cached blob, finds the cusEnt by ID, then uses JSON.SET on the exact path.
|
||||
@@ -70,6 +85,7 @@ export const setCachedCusEntField = async ({
|
||||
|
||||
/** Patch next_reset_at on a SubjectBalance in the V2 shared balance hash. */
|
||||
export const setCachedSubjectBalanceField = async ({
|
||||
ctx,
|
||||
orgId,
|
||||
env,
|
||||
customerId,
|
||||
@@ -79,6 +95,7 @@ export const setCachedSubjectBalanceField = async ({
|
||||
value,
|
||||
redisV2,
|
||||
}: {
|
||||
ctx?: TestContext;
|
||||
orgId: string;
|
||||
env: string;
|
||||
customerId: string;
|
||||
@@ -86,8 +103,15 @@ export const setCachedSubjectBalanceField = async ({
|
||||
customerEntitlementId: string;
|
||||
field: string;
|
||||
value: number | string | null;
|
||||
redisV2: Redis;
|
||||
redisV2?: Redis;
|
||||
}): Promise<void> => {
|
||||
const targetRedisV2 =
|
||||
redisV2 ??
|
||||
(ctx ? await getRoutedRedisForCustomer({ ctx, customerId }) : null);
|
||||
if (!targetRedisV2) {
|
||||
throw new Error("setCachedSubjectBalanceField requires redisV2 or ctx");
|
||||
}
|
||||
|
||||
const balanceKey = buildSharedFullSubjectBalanceKey({
|
||||
orgId,
|
||||
env,
|
||||
@@ -95,12 +119,12 @@ export const setCachedSubjectBalanceField = async ({
|
||||
featureId,
|
||||
});
|
||||
|
||||
const raw = await redisV2.hget(balanceKey, customerEntitlementId);
|
||||
const raw = await targetRedisV2.hget(balanceKey, customerEntitlementId);
|
||||
if (!raw) return;
|
||||
|
||||
const subjectBalance = JSON.parse(raw);
|
||||
subjectBalance[field] = value;
|
||||
await redisV2.hset(
|
||||
await targetRedisV2.hset(
|
||||
balanceKey,
|
||||
customerEntitlementId,
|
||||
JSON.stringify(subjectBalance),
|
||||
@@ -136,6 +160,7 @@ export const expireCusEntForReset = async ({
|
||||
}
|
||||
|
||||
const pastTime = pastTimeMs ?? Date.now() - 1000;
|
||||
const routedRedisV2 = await getRoutedRedisForCustomer({ ctx, customerId });
|
||||
|
||||
// Update Postgres
|
||||
await ctx.db
|
||||
@@ -162,7 +187,7 @@ export const expireCusEntForReset = async ({
|
||||
customerEntitlementId: cusEnt.id,
|
||||
field: "next_reset_at",
|
||||
value: pastTime,
|
||||
redisV2: ctx.redisV2,
|
||||
redisV2: routedRedisV2,
|
||||
});
|
||||
|
||||
return cusEnt;
|
||||
@@ -200,6 +225,7 @@ export const expireAllCusEntsForReset = async ({
|
||||
}
|
||||
|
||||
const pastTime = pastTimeMs ?? Date.now() - 1000;
|
||||
const routedRedisV2 = await getRoutedRedisForCustomer({ ctx, customerId });
|
||||
|
||||
for (const cusEnt of cusEnts) {
|
||||
await ctx.db
|
||||
@@ -224,7 +250,7 @@ export const expireAllCusEntsForReset = async ({
|
||||
customerEntitlementId: cusEnt.id,
|
||||
field: "next_reset_at",
|
||||
value: pastTime,
|
||||
redisV2: ctx.redisV2,
|
||||
redisV2: routedRedisV2,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
LATEST_VERSION,
|
||||
type OrgConfig,
|
||||
} from "@autumn/shared";
|
||||
import { getFeatures } from "@tests/setup/v2Features.js";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { resolveRedisV2 } from "@/external/redis/resolveRedisV2.js";
|
||||
@@ -29,11 +30,13 @@ export const createSubOrgTestContext = async ({
|
||||
testSecretKey,
|
||||
configOverrides,
|
||||
taxRegistrations,
|
||||
setupDefaultFeatures,
|
||||
}: {
|
||||
subOrgSlug: string;
|
||||
testSecretKey: string;
|
||||
configOverrides?: Partial<OrgConfig>;
|
||||
taxRegistrations?: TaxRegistrationCountry[];
|
||||
setupDefaultFeatures?: boolean;
|
||||
}): Promise<TestContext> => {
|
||||
const { db } = initDrizzle();
|
||||
|
||||
@@ -190,6 +193,14 @@ export const createSubOrgTestContext = async ({
|
||||
}
|
||||
}
|
||||
|
||||
if (setupDefaultFeatures) {
|
||||
await FeatureService.insert({
|
||||
db,
|
||||
data: Object.values(getFeatures({ orgId: subOrg.id })),
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Sub-org features.
|
||||
const features = await FeatureService.list({
|
||||
db,
|
||||
|
||||
@@ -111,6 +111,19 @@ const lazyDefaultCtx = new Proxy({} as TestContext, {
|
||||
}
|
||||
return Reflect.get(ctx, prop);
|
||||
},
|
||||
set(_target, prop, value, _receiver) {
|
||||
const ctx = (globalThis as { __autumnTestContext?: TestContext | null })
|
||||
.__autumnTestContext;
|
||||
if (ctx == null) {
|
||||
throw new Error(
|
||||
`Default TestContext is not initialized. The integration test ` +
|
||||
`preload (server/tests/setup-integration-tests.ts) did not ` +
|
||||
`populate globalThis.__autumnTestContext before "${String(prop)}" ` +
|
||||
`was assigned.`,
|
||||
);
|
||||
}
|
||||
return Reflect.set(ctx, prop, value);
|
||||
},
|
||||
has(_target, prop) {
|
||||
const ctx = (globalThis as { __autumnTestContext?: TestContext | null })
|
||||
.__autumnTestContext;
|
||||
@@ -125,7 +138,15 @@ const lazyDefaultCtx = new Proxy({} as TestContext, {
|
||||
const ctx = (globalThis as { __autumnTestContext?: TestContext | null })
|
||||
.__autumnTestContext;
|
||||
if (ctx == null) return undefined;
|
||||
return Reflect.getOwnPropertyDescriptor(ctx, prop);
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(ctx, prop);
|
||||
if (!descriptor) return undefined;
|
||||
|
||||
return {
|
||||
configurable: true,
|
||||
enumerable: descriptor.enumerable,
|
||||
writable: true,
|
||||
value: Reflect.get(ctx, prop),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ type PlatformCreateConfig = {
|
||||
userEmail?: string;
|
||||
configOverrides?: Partial<OrgConfig>;
|
||||
taxRegistrations?: TaxRegistrationCountry[];
|
||||
setupDefaultFeatures?: boolean;
|
||||
};
|
||||
|
||||
type ScenarioConfig = {
|
||||
@@ -789,6 +790,7 @@ const createAndRedeemReferralCode = ({
|
||||
* @param userEmail - Owner email; defaults to "platform-tests@autumn.test".
|
||||
* @param configOverrides - Merged into the sub-org's config jsonb.
|
||||
* @param taxRegistrations - Countries to register Stripe Tax for.
|
||||
* @param setupDefaultFeatures - Inserts standard test features on the sub-org.
|
||||
*
|
||||
* @example s.platform.create({ configOverrides: { automatic_tax: true }, taxRegistrations: ["AU"] })
|
||||
*/
|
||||
@@ -965,6 +967,7 @@ export async function initScenario({
|
||||
testSecretKey: response.test_secret_key,
|
||||
configOverrides: config.platformConfig.configOverrides,
|
||||
taxRegistrations: config.platformConfig.taxRegistrations,
|
||||
setupDefaultFeatures: config.platformConfig.setupDefaultFeatures,
|
||||
});
|
||||
|
||||
console.log(
|
||||
|
||||
Reference in New Issue
Block a user