Skip to main content

DOMAIN_MODEL — pricing-service

Sibling: SERVICE_OVERVIEW · APPLICATION_LOGIC · DATA_MODEL · EVENT_SCHEMAS

Strategic anchors: 02 Enterprise Architecture §3 · 04 Event-Driven Architecture §3 · 10 Payments Architecture · standards/NAMING · standards/ERROR_CODES

The domain model is pure TypeScript. No NestJS decorators, no Drizzle imports, no pg, no fetch, no process.env. The CI dependency-graph guard fails any PR that introduces such imports under src/domain/. The model below is the source of truth for invariants, value objects, the rate-rules engine, and the price derivation pipeline; the SQL projection and the wire DTOs are downstream of this.


1. Value objects

import { Branded } from '@ghasi/domain-primitives';

// IDs
export type TenantId = Branded<string, 'TenantId'>; // tnt_…
export type PropertyId = Branded<string, 'PropertyId'>; // ppt_…
export type RoomTypeId = Branded<string, 'RoomTypeId'>; // rmt_…
export type RatePlanId = Branded<string, 'RatePlanId'>; // rate_…
export type RateRuleId = Branded<string, 'RateRuleId'>; // rru_…
export type DiscountId = Branded<string, 'DiscountId'>; // dsc_…
export type PromotionId = Branded<string, 'PromotionId'>; // prm_…
export type TaxRuleId = Branded<string, 'TaxRuleId'>; // tax_…
export type FeeRuleId = Branded<string, 'FeeRuleId'>; // fee_…
export type PriceQuoteId = Branded<string, 'PriceQuoteId'>; // qte_…
export type FxSnapshotId = Branded<string, 'FxSnapshotId'>; // fxs_…
export type DynamicSuggestionId = Branded<string, 'DynamicSuggestionId'>; // dps_…
export type ReservationId = Branded<string, 'ReservationId'>; // rsv_… (reference only)

// Money — bigint micro-units; rate plan currency is the base of derivation
export interface Money {
amountMicro: bigint; // e.g. 12_500_000n = 12.50
currency: CurrencyCode;
}

export type CurrencyCode = 'USD' | 'EUR' | 'IRR' | 'AFN' | 'TJS' | 'PKR' | 'GBP' | 'AED' | 'SAR';

export const CURRENCY_ROUNDING: Record<CurrencyCode, bigint> = {
USD: 10_000n, // round to 0.01 (10_000 micro)
EUR: 10_000n,
GBP: 10_000n,
AED: 10_000n,
SAR: 10_000n,
AFN: 1_000_000n, // round to 1.00
TJS: 10_000n,
PKR: 1_000_000n,
IRR: 1_000_000_000n, // round to 1000 IRR (high inflation; nearest 1000 unit)
};

// Date / time
export interface DateRange { start: ISODate; end: ISODate; } // [start, end) — start inclusive, end exclusive
export interface StayWindow extends DateRange { nights: number; }
export type DayOfWeek = 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun';

// Channel scope
export type ChannelScope = 'direct' | 'meta' | 'walk_in' | 'phone_by_staff' | 'ota' | 'all';

// FX snapshot
export interface FxRateSnapshot {
id: FxSnapshotId;
base: CurrencyCode;
quote: CurrencyCode;
rate: number; // 1 base = rate * quote
source: 'provider:ecb' | 'provider:openexchange' | 'tenant_pinned' | 'manual_override';
capturedAt: ISODate; // last refresh
staleAfter: ISODate; // capturedAt + staleAfterHours
hardExpireAt: ISODate; // capturedAt + hardExpireHours
providerRef?: string; // upstream provider response id
}

All value objects are constructed via factory functions that throw InvalidValueError on validation failure. Money arithmetic is closed over the same currency only; cross-currency math goes through FxRateSnapshot.


2. Aggregates and entities

2.1 RatePlan (aggregate root)

export interface RatePlan {
id: RatePlanId;
tenantId: TenantId;
propertyId: PropertyId;

code: string; // e.g. 'BAR', 'WEEKLY', 'GOV-AF', 'CORP-ACME', 'NREF', 'PKG-HALAL'
displayName: LocalizedString; // {ps,fa,ar,en}
category: RatePlanCategory;
channelScope: ChannelScope[]; // empty == 'all'
currency: CurrencyCode; // base currency of this plan
refundability: Refundability;
depositPolicy: DepositPolicy;
advancePurchaseDays?: number; // booking must be made N days before stay start
minLOS?: number; // minimum nights
maxLOS?: number;
bookableFrom?: ISODate;
bookableUntil?: ISODate;
shariaCompliant: boolean; // forbids compounding deposits, riba-tagged fees
packageInclusions?: PackageInclusion[]; // breakfast, halal-meal-plan, airport-pickup
status: RatePlanStatus; // §3 state machine
version: number; // OCC + immutable-after-publish version stamp
publishedAt?: ISODate;
publishedBy?: ActorRef;
archivedAt?: ISODate;
archivedReason?: string;
createdAt: ISODate;
createdBy: ActorRef;
updatedAt: ISODate;
updatedBy: ActorRef;

// Composition (loaded as needed)
rules: RateRule[];
roomTypeLinks: RatePlanRoomType[];
discounts: Discount[];
appliesToFeeRuleIds: FeeRuleId[];
appliesToTaxRuleIds: TaxRuleId[];
}

export type RatePlanCategory =
| 'bar' // Best Available Rate
| 'weekly' // Weekly stay rate (LOS-baked)
| 'government' // Gov agency rate
| 'corporate' // Corporate negotiated
| 'non_refundable'
| 'package' // Room + something
| 'group' // Group block rate
| 'promotional'; // Time-bound promo

export type RatePlanStatus = 'draft' | 'published' | 'archived';

export type Refundability =
| { kind: 'fully_refundable'; cutoffHoursBeforeStart: number }
| { kind: 'partially_refundable'; cutoffHoursBeforeStart: number; penaltyPct: number }
| { kind: 'non_refundable' };

export type DepositPolicy =
| { kind: 'none' }
| { kind: 'flat'; amountMicro: bigint; currency: CurrencyCode }
| { kind: 'pct_of_total'; pct: number } // 0..1
| { kind: 'first_night' };
// NOTE: kind 'compounding' is intentionally absent — compounding deposits are not allowed,
// regardless of shariaCompliant flag.

export type PackageInclusion =
| { kind: 'breakfast'; halal: boolean; perNight: boolean }
| { kind: 'meal_plan'; type: 'half_board' | 'full_board' | 'all_inclusive'; halal: boolean }
| { kind: 'airport_pickup'; oneWay: boolean }
| { kind: 'late_checkout'; latestHourLocal: number };

2.2 RateRule

export interface RateRule {
id: RateRuleId;
tenantId: TenantId;
ratePlanId: RatePlanId;
priority: number; // higher wins on tie; deterministic
scope: RateRuleScope;
baseMicro: bigint; // base nightly micro in plan currency
multiplier: number; // applied multiplicatively (default 1.0)
surchargeMicro: bigint; // applied additively (default 0)
losDiscount?: LosDiscount; // optional rule-level LOS discount
source: 'manual' | 'imported' | 'ai_accepted';
aiProvenance?: AIProvenance; // present when source == 'ai_accepted'
notes?: string; // <= 500 chars
version: number; // OCC
createdAt: ISODate;
createdBy: ActorRef;
updatedAt: ISODate;
updatedBy: ActorRef;
}

export interface RateRuleScope {
dateRange: DateRange;
daysOfWeek?: DayOfWeek[]; // empty/absent == all
roomTypeIds?: RoomTypeId[]; // empty/absent == all linked types
occupancyBand?: { adultsMin?: number; adultsMax?: number };
}

export interface LosDiscount {
thresholdNights: number; // applies when stay nights >= threshold
kind: 'pct' | 'flat';
amount: number; // pct: 0..1; flat: micro per night
}

2.3 RatePlanRoomType (linkage)

export interface RatePlanRoomType {
tenantId: TenantId;
ratePlanId: RatePlanId;
roomTypeId: RoomTypeId;
baseRoomTypeMultiplier?: number; // optional bias (e.g. SUITE 1.5x of base)
enabled: boolean;
linkedAt: ISODate;
linkedBy: ActorRef;
}

2.4 Discount

export interface Discount {
id: DiscountId;
tenantId: TenantId;
ratePlanId: RatePlanId;
kind: DiscountKind;
config: DiscountConfig; // discriminated by kind
enabled: boolean;
priorityInPipeline: number; // explicit ordering (lower = applied earlier)
createdAt: ISODate;
updatedAt: ISODate;
}

export type DiscountKind = 'los' | 'advance_purchase' | 'last_minute' | 'loyalty' | 'corporate_negotiated';

export type DiscountConfig =
| { kind: 'los'; thresholdNights: number; pct?: number; flatMicroPerNight?: bigint }
| { kind: 'advance_purchase'; minDaysBefore: number; pct?: number; flatMicro?: bigint }
| { kind: 'last_minute'; maxDaysBefore: number; markupPct: number /* note: markup, not discount */ }
| { kind: 'loyalty'; tier: 'silver' | 'gold' | 'platinum'; pct: number }
| { kind: 'corporate_negotiated'; corporateClientId: string; pct: number };

2.5 Promotion

export interface Promotion {
id: PromotionId;
tenantId: TenantId;
code: string; // unique per tenant (case-insensitive)
displayName: LocalizedString;
validFrom: ISODate;
validUntil: ISODate; // exclusive
applicableRatePlanIds?: RatePlanId[]; // empty == all
applicablePropertyIds?: PropertyId[]; // empty == all
applicableChannels?: ChannelScope[]; // empty == all
usageCap: number; // hard cap; 0 == unlimited
usageCount: number; // server-tracked counter (race-safe via OCC)
perGuestCap?: number; // optional (looked up via reservation history; best-effort)
discountPct?: number; // 0..1
discountFlatMicro?: bigint; // mutually exclusive with pct
currency?: CurrencyCode; // required if flat
status: PromotionStatus; // draft | active | inactive | expired
shariaCompliant: boolean; // mirrors plan flag; forbids riba-tagged copy
createdAt: ISODate;
updatedAt: ISODate;
}

export type PromotionStatus = 'draft' | 'active' | 'inactive' | 'expired';

2.6 TaxRule and FeeRule

export interface TaxRule {
id: TaxRuleId;
tenantId: TenantId;
jurisdiction: { country: 'AF' | 'TJ' | 'IR' | 'PK' | 'AE' | 'SA' | 'GB' | 'US' | string; region?: string };
scope: 'room' | 'fee' | 'all';
appliesToPropertyIds?: PropertyId[]; // empty == all
category: 'vat' | 'tourism' | 'hotel_tax' | 'service_tax';
rate: TaxRate; // pct or flat
inclusiveOfDisplayPrice: boolean;
validFrom: ISODate;
validUntil?: ISODate;
createdAt: ISODate;
updatedAt: ISODate;
}

export type TaxRate =
| { kind: 'pct'; pct: number } // 0..1
| { kind: 'flat'; amountMicro: bigint; currency: CurrencyCode };

export interface FeeRule {
id: FeeRuleId;
tenantId: TenantId;
propertyId?: PropertyId; // null == all properties of tenant
category: 'resort' | 'cleaning' | 'service' | 'tourism_bed' | 'late_checkout' | 'other';
rate: FeeRate;
cadence: 'per_night' | 'per_stay';
inclusiveOfDisplayPrice: boolean;
appliesToRatePlanIds?: RatePlanId[]; // empty == all
validFrom: ISODate;
validUntil?: ISODate;
shariaTag?: 'halal' | 'riba_forbidden' | null; // 'riba_forbidden' refuses application on shariaCompliant plans
createdAt: ISODate;
updatedAt: ISODate;
}

export type FeeRate =
| { kind: 'flat'; amountMicro: bigint; currency: CurrencyCode }
| { kind: 'pct_of_room'; pct: number };

2.7 PriceQuote

export interface PriceQuote {
id: PriceQuoteId;
tenantId: TenantId;
propertyId: PropertyId;
requestedAt: ISODate;
expiresAt: ISODate; // requestedAt + ttlSeconds (default 1800)
ttlSeconds: number;
status: 'live' | 'expired' | 'redeemed' | 'stale';
staleReason?: 'inventory_failed' | 'fx_invalidated' | 'rate_plan_archived';

request: QuoteRequestSnapshot; // full normalized inputs
derivation: QuoteDerivation; // every step's intermediate
ratePlan: { id: RatePlanId; version: number; snapshotName: string };
fxSnapshot?: { id: FxSnapshotId; base: CurrencyCode; quote: CurrencyCode; rate: number; capturedAt: ISODate; stale: boolean };
promoApplied?: { id: PromotionId; code: string; redemptionId: string };

totals: QuoteTotals;
redeemedByReservationId?: ReservationId;
redeemedAt?: ISODate;
}

export interface QuoteRequestSnapshot {
roomTypeIds: RoomTypeId[];
stayWindow: StayWindow;
occupants: Array<{ adults: number; children: number; infants: number }>;
channel: ChannelScope;
ratePlanHint?: RatePlanId;
promoCode?: string;
displayCurrency?: CurrencyCode;
}

export interface QuoteDerivation {
steps: DerivationStep[]; // ordered; one per pipeline stage (§ SERVICE_OVERVIEW.md §6)
shariaGuardPasses: boolean;
}

export interface DerivationStep {
step: 'resolve_rate_plan' | 'derive_nightly_base' | 'apply_discounts'
| 'compose_fees' | 'compose_taxes' | 'apply_fx' | 'sharia_guard' | 'pin';
inputs: Record<string, unknown>;
outputs: Record<string, unknown>;
notes?: string;
}

export interface QuoteTotals {
subtotal: Money; // sum of nightly post-multiplier post-surcharge
discountTotal: Money; // sum of all discount lines (positive = subtraction)
feeTotal: Money; // sum of exclusive fees only
taxTotal: Money; // sum of exclusive taxes only
inclusiveAdjustments: Money;// inclusive fees+taxes already inside subtotal
grandTotal: Money; // base currency
inDisplayCurrency?: Money; // present when displayCurrency != ratePlan.currency
}

2.8 DynamicPricingSuggestion

export interface DynamicPricingSuggestion {
id: DynamicSuggestionId;
tenantId: TenantId;
propertyId: PropertyId;
roomTypeId: RoomTypeId;
date: ISODate;
baselineRateMicro: bigint;
suggestedRange: { lowMicro: bigint; midMicro: bigint; highMicro: bigint };
currency: CurrencyCode;
signals: SuggestionSignals;
rationale: string; // short LLM explanation, locale-agnostic
status: 'generated' | 'accepted' | 'rejected' | 'expired';
expiresAt: ISODate; // suggestions are stale after 24 h
acceptedRule?: RateRuleId; // present when accepted; the materialized override
acceptedBy?: ActorRef;
acceptedAt?: ISODate;
rejectedBy?: ActorRef;
rejectedAt?: ISODate;
rejectedReason?: string;
aiProvenance: AIProvenance;
createdAt: ISODate;
}

export interface SuggestionSignals {
occupancyPct: number; // 0..1 forecast for the date
bookingPace: number; // bookings/day in last 7 days vs historical
competitorRateProxy?: number; // optional, in plan currency, micro
eventsNearby: Array<{ kind: 'religious' | 'cultural' | 'sport' | 'business'; name: string; weight: number }>;
}

3. RatePlan state machine

┌─────────┐ PublishRatePlan ┌────────────┐
│ draft │ ───────────────────▶ │ published │ ───┐
└────┬────┘ └────┬───────┘ │ ArchiveRatePlan
│ EditRatePlan (unlimited) │ EditRatePlan (rules append-only;
▼ │ core fields locked)
┌─────────┐ ▼
│ draft │ ┌────────────┐
└─────────┘ │ archived │ (terminal; future bookings preserve quote)
└────────────┘
FromToCommandGuards
draftpublishedPublishRatePlanrule-set passes validation; at least one room-type linkage; currency set; shariaGuard passes if shariaCompliant
draftarchivedArchiveRatePlanalways allowed
publishedpublishedEditRatePlan (append rule, edit non-core fields)core-field set unchanged; OCC version increments
publishedarchivedArchiveRatePlanalways; existing pinned quotes survive until TTL; new derivations refuse
archived(none)terminal

Any transition not in the table raises MELMASTOON.PRICING.RATE_PLAN_LOCKED or MELMASTOON.PRICING.RATE_PLAN_INACTIVE.


4. Promotion state machine

draft ──Activate──▶ active ──Deactivate──▶ inactive
│ │ │
│ │ validUntil < now │ validUntil < now
▼ ▼ ▼
expired (terminal) expired expired

active promotions are race-checked at redemption: usageCount < usageCap (usageCap=0 means unlimited).


5. Invariants (enforced in domain code)

#InvariantError code
I1Every aggregate carries matching tenantId; constructors refuse missing/mismatched valuesMELMASTOON.PRICING.CROSS_TENANT_REFERENCE
I2Published RatePlan cannot mutate currency, shariaCompliant, refundability, channelScope, categoryMELMASTOON.PRICING.RATE_PLAN_LOCKED
I3RatePlan.publish requires ≥1 RatePlanRoomType enabled and a valid rule cascadeMELMASTOON.GENERAL.VALIDATION_FAILED
I4RateRule.priority is a positive integer; ties broken by (createdAt asc, id asc) deterministicallyMELMASTOON.PRICING.RULE_PRIORITY_INVALID
I5Discount cascade output never falls below floorMicro (default 1 unit of plan currency); overflow short-circuitsMELMASTOON.PRICING.DERIVATION_FAILED (detail discount_overflow)
I6Promotion.usageCount <= Promotion.usageCap (when cap > 0)MELMASTOON.PRICING.PROMO_OVEROBLIGATION
I7Promotion.code unique per tenant (case-insensitive)MELMASTOON.PRICING.PROMO_CODE_COLLISION
I8TaxRule validity intervals do not overlap for the same (tenant, jurisdiction, scope, category) tripleMELMASTOON.PRICING.TAX_RULE_OVERLAP
I9FxRateSnapshot.rate > 0; refresh advances capturedAt; refusing staleAfter < hardExpireAt orderingMELMASTOON.PRICING.FX_SNAPSHOT_INVALID
I10PriceQuote.expiresAt > requestedAt; once now > expiresAt quote is immutableMELMASTOON.PRICING.QUOTE_EXPIRED
I11PriceQuote.derivation is complete and reproducible (re-run yields identical totals when inputs unchanged)MELMASTOON.PRICING.DERIVATION_NON_DETERMINISTIC
I12DynamicPricingSuggestion.acceptedRule may only be created via AcceptDynamicPricingSuggestionUseCase (not by direct rate-rule mutation)MELMASTOON.AI.HITL_REQUIRED
I13On shariaCompliant plans, no FeeRule.shariaTag == 'riba_forbidden' may apply; no compounding deposit policy may be selectedMELMASTOON.PRICING.SHARIA_GUARD_FAILED
I14Money arithmetic across different currencies refuses without an explicit FxRateSnapshotMELMASTOON.PRICING.CURRENCY_MISMATCH
I15version strictly increases on every save; mismatch raises stale-versionMELMASTOON.PRICING.STALE_VERSION

6. Domain events (names; payloads in EVENT_SCHEMAS)

EventEmitted on
RatePlanCreatedCreateRatePlanUseCase
RatePlanUpdatedUpdateRatePlanUseCase
RatePlanArchivedArchiveRatePlanUseCase
RateRuleCreatedAppendRateRuleUseCase
RateRuleUpdatedUpdateRateRuleUseCase
PromotionCreatedCreatePromotionUseCase
PromotionActivatedActivatePromotionUseCase
PromotionDeactivatedDeactivatePromotionUseCase
QuoteRequestedquote request received (analytics)
QuoteCreatedquote pinned
QuoteExpiredsweeper / inventory-fail invalidation
TaxRuleUpdatedUpsertTaxRuleUseCase
FxSnapshotUpdatedRefreshFxSnapshotUseCase
DynamicSuggestionGeneratedsuggestion ingested from ai-orchestrator-service
DynamicSuggestionAcceptedrevenue-manager HITL accept

All events are wrapped in the canonical envelope from 04 §4. The causationId is the event that triggered the transition (e.g., ai.suggestion.dynamic_pricing.v1 for DynamicSuggestionGenerated).


7. Domain errors (catalog excerpt)

CodeMeaning
MELMASTOON.PRICING.RATE_PLAN_NOT_FOUNDRatePlan does not exist or is not active for the requested date
MELMASTOON.PRICING.RATE_PLAN_INACTIVERatePlan is draft or archived
MELMASTOON.PRICING.RATE_PLAN_LOCKEDAttempt to mutate immutable field on published plan
MELMASTOON.PRICING.RULE_PRIORITY_INVALIDRule priority is not a positive integer
MELMASTOON.PRICING.QUOTE_EXPIREDQuote TTL elapsed
MELMASTOON.PRICING.QUOTE_STALEQuote invalidated by inventory or FX event
MELMASTOON.PRICING.PROMO_OVEROBLIGATIONRedemption would exceed usageCap
MELMASTOON.PRICING.PROMO_CODE_COLLISIONPromo code already exists for tenant
MELMASTOON.PRICING.PROMO_NOT_APPLICABLEPromo scope/property/channel mismatch
MELMASTOON.PRICING.TAX_RULE_OVERLAPConflicting validity intervals
MELMASTOON.PRICING.CURRENCY_MISMATCHCross-currency math without FX snapshot
MELMASTOON.PRICING.FX_SNAPSHOT_STALESnapshot exceeds hard expire and no override
MELMASTOON.PRICING.FX_SNAPSHOT_INVALIDSnapshot fails value/order invariants
MELMASTOON.PRICING.DERIVATION_FAILEDPipeline could not derive a final amount
MELMASTOON.PRICING.DERIVATION_NON_DETERMINISTICI11 violated in test/replay
MELMASTOON.PRICING.SHARIA_GUARD_FAILEDDerivation would violate Sharia constraints
MELMASTOON.PRICING.STALE_VERSIONOCC mismatch
MELMASTOON.PRICING.CROSS_TENANT_REFERENCEAggregate referenced another tenant's id

8. Domain services (pure functions over aggregates; no I/O)

// rate-rules-engine.ts — the heart of the derivation pipeline
export interface NightlyDerivation {
date: ISODate;
roomTypeId: RoomTypeId;
ruleId: RateRuleId;
baseMicro: bigint;
multiplier: number;
surchargeMicro: bigint;
preDiscountMicro: bigint; // baseMicro * multiplier + surchargeMicro
appliedDiscounts: Array<{ kind: DiscountKind; amountMicro: bigint }>;
postDiscountMicro: bigint; // never below floorMicro
appliedFeesInclusiveMicro: bigint;
appliedTaxesInclusiveMicro: bigint;
feeBreakdown: Array<{ feeRuleId: FeeRuleId; amountMicro: bigint; inclusive: boolean }>;
taxBreakdown: Array<{ taxRuleId: TaxRuleId; amountMicro: bigint; inclusive: boolean }>;
}

// Pure: given rules + request, returns nightly derivations deterministically
export function deriveNightly(
request: QuoteRequestSnapshot,
plan: RatePlan,
roomTypeBias: Map<RoomTypeId, number>,
feeRules: FeeRule[],
taxRules: TaxRule[],
fxSnapshot: FxRateSnapshot | null,
clock: { today: ISODate },
floorMicro: bigint,
): NightlyDerivation[] { /* … pure code … */ }

// Tie-break: deterministic by (priority desc, scopeSpecificity desc, createdAt asc, id asc)
export function selectRule(rules: RateRule[], date: ISODate, dow: DayOfWeek, roomTypeId: RoomTypeId): RateRule | null;

// Sharia guard
export function shariaGuard(plan: RatePlan, deposit: DepositPolicy, fees: FeeRule[]): { ok: true } | { ok: false; reason: string };

// Promo redemption check (race-safe at infrastructure layer; this is the pure check)
export function canRedeem(promo: Promotion, request: QuoteRequestSnapshot, now: ISODate): boolean;

// Discount cascade (pure)
export function applyDiscounts(
preDiscountMicro: bigint,
discounts: Discount[],
request: QuoteRequestSnapshot,
now: ISODate,
loyaltyTier: 'silver' | 'gold' | 'platinum' | null,
floorMicro: bigint,
): { postDiscountMicro: bigint; lines: Array<{ kind: DiscountKind; amountMicro: bigint }> };

The rate-rules engine is fully unit-tested with property-based tests (fast-check) for monotonicity (LOS discount never increases price), idempotency (re-derivation stable), and invariant preservation (output never < floor).


9. Notes on rate-plan archival with active future bookings

When a RatePlan is archived:

  • Existing pinned PriceQuotes remain valid until their TTL.
  • Existing Reservations referencing the plan are unaffected — reservation-service holds the pinned snapshot.
  • New quote requests with ratePlanHint=<archivedId> return MELMASTOON.PRICING.RATE_PLAN_INACTIVE.
  • Search-aggregation receives rate_plan.archived.v1 and removes the plan from listings.
  • Audit row (actor, archivedAt, archivedReason) is mandatory.

10. Test surface (unit-only here; broader tests in TESTING_STRATEGY)

The aggregates ship with these unit tests under test/unit/domain/:

  • rate-plan.state-machine.spec.ts — every legal and illegal transition.
  • rate-plan.invariants.spec.ts — I1..I3, I13, I15.
  • rate-rule.tie-break.spec.ts — deterministic priority/specificity ordering.
  • rate-rules-engine.derive-nightly.spec.ts — golden cases per rate-plan category.
  • discount-cascade.property.spec.ts — fast-check properties (monotonicity, floor preservation).
  • promotion.race.spec.ts — usage-cap honoring under simulated concurrency (pure, stub clock).
  • promotion.applicability.spec.ts — scope checks (property, channel, plan, dates).
  • tax-rule.overlap.spec.ts — validity-interval invariant.
  • fx-snapshot.spec.ts — freshness windows; rounding policy per currency.
  • sharia-guard.spec.ts — refusal cases (riba-tagged fee, compounding deposit).
  • quote.derivation.deterministic.spec.ts — replay yields identical totals (I11).
  • dynamic-suggestion.hitl.spec.ts — accepting a suggestion materializes a RateRule with source='ai_accepted' and provenance preserved.

All tests use __builders__/ fixtures colocated under src/domain/. No test imports NestJS, Drizzle, or any I/O.