Files
cfw-autumn/shared/models/cusModels/fullSubject/normalizedFullSubjectModel.ts
2026-05-13 17:24:08 +08:00

188 lines
6.6 KiB
TypeScript

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 { 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<string, EntityBalance> | 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<string, AggregatedSubjectFlag>;
};
/**
* 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[];
flags: Record<string, SubjectFlag>;
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[];
};