import { z } from "zod/v4"; import { type AggregatedFeatureBalance, AggregatedFeatureBalanceSchema, } from "../../cusProductModels/cusEntModels/aggregatedCusEnt.js"; import { type EntityBalance, FullCustomerEntitlementSchema, } from "../../cusProductModels/cusEntModels/cusEntModels.js"; import type { Replaceable } from "../../cusProductModels/cusEntModels/replaceableTable.js"; import type { DbRollover } from "../../cusProductModels/cusEntModels/rolloverModels/rolloverTable.js"; import type { UsageWindow } from "../../cusProductModels/cusEntModels/usageWindowTable.js"; import type { FullCustomerPrice } from "../../cusProductModels/cusPriceModels/cusPriceModels.js"; import { FullCustomerPriceSchema } from "../../cusProductModels/cusPriceModels/cusPriceModels.js"; import type { DbCustomerPrice } from "../../cusProductModels/cusPriceModels/cusPriceTable.js"; import { CusProductSchema, type FeatureOptions, FeatureOptionsSchema, } from "../../cusProductModels/cusProductModels.js"; import type { DbCustomerProduct } from "../../cusProductModels/cusProductTable.js"; import type { MigrationItemRunData } from "../../migrationV2Models/migrationItemRunSchema.js"; import type { EntitlementWithFeature } from "../../productModels/entModels/entModels.js"; import type { DbFreeTrial } from "../../productModels/freeTrialModels/freeTrialTable.js"; import type { DbPrice } from "../../productModels/priceModels/priceTable.js"; import type { DbProduct } from "../../productModels/productTable.js"; import type { Subscription } from "../../subModels/subModels.js"; import type { Customer } from "../cusModels.js"; import type { Entity } from "../entityModels/entityModels.js"; import type { Invoice } from "../invoiceModels/invoiceModels.js"; import type { SubjectType } from "./fullSubjectModel.js"; /** * Schema mirror of the `SubjectFlag` shape. Used by the cached-payload * schema walker to know where nullable positions are. */ export const SubjectFlagSchema = z.object({ featureId: z.string(), internalFeatureId: z.string(), entitlementId: z.string(), customerEntitlementId: z.string(), customerProductId: z.string().nullable(), internalCustomerId: z.string(), internalEntityId: z.string().nullable(), expiresAt: z.number().nullable(), externalId: z.string().nullable(), }); export type SubjectFlag = { featureId: string; internalFeatureId: string; entitlementId: string; customerEntitlementId: string; customerProductId: string | null; internalCustomerId: string; internalEntityId: string | null; expiresAt: number | null; externalId: string | null; }; /** * Schema mirror of `SubjectBalance`. Extends `FullCustomerEntitlementSchema` * with the helper fields attached during normalization (customerPrice, * customerProductOptions, customerProductQuantity, isEntityLevel). * * Used by the cache-hole-filling walker; this is not a validator — the * runtime `SubjectBalance` type below is the source of truth. */ export const SubjectBalanceSchema = FullCustomerEntitlementSchema.extend({ customerPrice: FullCustomerPriceSchema.nullable(), customerProductOptions: FeatureOptionsSchema.nullable(), customerProductQuantity: z.number(), isEntityLevel: z.boolean(), }); export type SubjectBalance = { id: string; customer_product_id: string | null; entitlement_id: string; internal_customer_id: string; internal_entity_id: string | null; internal_feature_id: string; feature_id: string; unlimited: boolean | null; balance: number; adjustment: number | null; additional_balance: number; usage_allowed: boolean | null; next_reset_at: number | null; expires_at: number | null; external_id: string | null; entities: Record | null; cache_version: number | null; created_at: number; customer_id?: string | null; entitlement: EntitlementWithFeature; replaceables: Replaceable[]; rollovers: DbRollover[]; customerPrice: FullCustomerPrice | null; customerProductOptions: FeatureOptions | null; customerProductQuantity: number; isEntityLevel: boolean; }; /** * Identity-only view of a boolean feature aggregated across all entity-level * grants. Sibling of `AggregatedFeatureBalance`, but for booleans — no balance * fields since booleans don't have them. Produced by splitting the entity * aggregate CTE output by feature type. */ export const AggregatedSubjectFlagSchema = z.object({ feature_id: z.string(), internal_feature_id: z.string(), internal_customer_id: z.string(), api_id: z.string(), }); export type AggregatedSubjectFlag = { feature_id: string; internal_feature_id: string; internal_customer_id: string; api_id: string; }; /** * Schema mirror of `EntityAggregations`. Reuses `CusProductSchema` as the Zod * mirror of `DbCustomerProduct` for the aggregated customer products array. */ export const EntityAggregationsSchema = z.object({ aggregated_customer_products: z.array(CusProductSchema), aggregated_customer_entitlements: z.array(AggregatedFeatureBalanceSchema), aggregated_subject_flags: z .record(z.string(), AggregatedSubjectFlagSchema) .default({}), }); export type EntityAggregations = { aggregated_customer_products: DbCustomerProduct[]; aggregated_customer_entitlements: AggregatedFeatureBalance[]; aggregated_subject_flags: Record; }; /** * Normalized (flat-array) representation of a customer or entity subject. * * - `customer_entitlements` contains metered balances only (product + extra * combined, distinguished by `customer_product_id` being null for extras). * - Boolean CEs are collapsed into `flags`. * - Each `SubjectBalance` carries the pre-resolved context needed by the * deduction path, so each Redis hash field can be self-contained. * - Catalog arrays (`products`, `entitlements`, `prices`, `free_trials`) are * deduplicated reference data shared across the subject. * * This is the shape stored in Redis (split across a subject STRING + per-feature * balance HASHes) and the shape the DB query naturally returns. */ export type NormalizedFullSubject = { subjectType: SubjectType; customerId: string; internalCustomerId: string; entityId?: string; internalEntityId?: string; customer: Customer; entity?: Entity; customer_products: DbCustomerProduct[]; customer_entitlements: SubjectBalance[]; customer_prices: DbCustomerPrice[]; /** Windowed-cap counter rows for ALL scopes (customer + entity; * internal_entity_id null = customer scope), live-read from the * per-feature balance hashes' `_usage_windows` field — never the cached * subject view. `normalizedToFullSubject` narrows to the subject's scope. */ usage_windows: UsageWindow[]; flags: Record; products: DbProduct[]; entitlements: EntitlementWithFeature[]; prices: DbPrice[]; free_trials: DbFreeTrial[]; subscriptions: Subscription[]; invoices: Invoice[]; entity_aggregations?: EntityAggregations; /** Latest 10 `migration_item_runs` for this customer, scoped to the * org's active lazy/background migrations. Empty when no migrations * are active for the org. */ migration_item_runs?: MigrationItemRunData[]; };