$selected_extras Selected extras. * @param int $attendees Attendee count. * @param Money $group_flat_fee Group flat fee (zero if unused). * @param Money $group_per_attendee Per-attendee group price (zero if unused). * @param Coupon|null $coupon Coupon to apply when valid. * @param int|null $service_id Service id for coupon scoping. * @param int|null $agent_id Agent id for coupon scoping. * @param int|null $customer_id Customer id for per-customer limits. * @param int $customer_coupon_usage Prior uses of this coupon by the customer. * @param DateTimeImmutable $now Clock for coupon validity (UTC). * @param TaxRate $tax_rate Rate for the service tax class. * @param DepositType $deposit_type Deposit configuration. * @param int $deposit_value Fixed minor units or percent 0–100. * @param PriceChargeMode $charge_mode What to collect now. * * @throws DomainException On invariant breach or currency mismatch. */ public function __construct( private string $currency, private Money $base_price, private ?Money $agent_price_override, private array $selected_extras, private int $attendees, private Money $group_flat_fee, private Money $group_per_attendee, private ?Coupon $coupon, private ?int $service_id, private ?int $agent_id, private ?int $customer_id, private int $customer_coupon_usage, private DateTimeImmutable $now, private TaxRate $tax_rate, private DepositType $deposit_type, private int $deposit_value, private PriceChargeMode $charge_mode, ) { $currency = strtoupper( trim( $this->currency ) ); if ( 1 !== preg_match( '/^[A-Z]{3}$/', $currency ) ) { throw new DomainException( 'pricing.currency_invalid', 'Currency must be a 3-letter ISO 4217 code.' ); } $this->currency = $currency; $this->assert_currency( $this->base_price ); if ( null !== $this->agent_price_override ) { $this->assert_currency( $this->agent_price_override ); } foreach ( $this->selected_extras as $extra ) { $this->assert_currency( $extra->unit_price() ); } $this->assert_currency( $this->group_flat_fee ); $this->assert_currency( $this->group_per_attendee ); if ( $this->attendees < 1 ) { throw new DomainException( 'pricing.attendees', 'attendees must be at least 1.' ); } if ( $this->customer_coupon_usage < 0 ) { throw new DomainException( 'pricing.coupon_usage', 'customer_coupon_usage must not be negative.' ); } if ( DepositType::Percent === $this->deposit_type && ( $this->deposit_value < 0 || $this->deposit_value > 100 ) ) { throw new DomainException( 'pricing.deposit_percent', 'Percent deposit must be 0–100.' ); } if ( DepositType::Fixed === $this->deposit_type && $this->deposit_value < 0 ) { throw new DomainException( 'pricing.deposit_fixed', 'Fixed deposit must not be negative.' ); } if ( DepositType::None === $this->deposit_type && 0 !== $this->deposit_value ) { throw new DomainException( 'pricing.deposit_none', 'Deposit value must be 0 when type is none.' ); } if ( null !== $this->service_id && $this->service_id < 1 ) { throw new DomainException( 'pricing.service_id', 'service_id must be positive when set.' ); } if ( null !== $this->agent_id && $this->agent_id < 1 ) { throw new DomainException( 'pricing.agent_id', 'agent_id must be positive when set.' ); } if ( null !== $this->customer_id && $this->customer_id < 1 ) { throw new DomainException( 'pricing.customer_id', 'customer_id must be positive when set.' ); } } /** * ISO currency. * * @since 0.1.0 */ public function currency(): string { return $this->currency; } /** * Base duration price. * * @since 0.1.0 */ public function base_price(): Money { return $this->base_price; } /** * Agent price override. * * @since 0.1.0 */ public function agent_price_override(): ?Money { return $this->agent_price_override; } /** * Selected extras. * * @return list * * @since 0.1.0 */ public function selected_extras(): array { return $this->selected_extras; } /** * Attendees. * * @since 0.1.0 */ public function attendees(): int { return $this->attendees; } /** * Group flat fee. * * @since 0.1.0 */ public function group_flat_fee(): Money { return $this->group_flat_fee; } /** * Group per-attendee price. * * @since 0.1.0 */ public function group_per_attendee(): Money { return $this->group_per_attendee; } /** * Coupon. * * @since 0.1.0 */ public function coupon(): ?Coupon { return $this->coupon; } /** * Service id. * * @since 0.1.0 */ public function service_id(): ?int { return $this->service_id; } /** * Agent id. * * @since 0.1.0 */ public function agent_id(): ?int { return $this->agent_id; } /** * Customer id. * * @since 0.1.0 */ public function customer_id(): ?int { return $this->customer_id; } /** * Customer coupon usage count. * * @since 0.1.0 */ public function customer_coupon_usage(): int { return $this->customer_coupon_usage; } /** * Evaluation clock. * * @since 0.1.0 */ public function now(): DateTimeImmutable { return $this->now; } /** * Tax rate. * * @since 0.1.0 */ public function tax_rate(): TaxRate { return $this->tax_rate; } /** * Deposit type. * * @since 0.1.0 */ public function deposit_type(): DepositType { return $this->deposit_type; } /** * Deposit value. * * @since 0.1.0 */ public function deposit_value(): int { return $this->deposit_value; } /** * Charge mode. * * @since 0.1.0 */ public function charge_mode(): PriceChargeMode { return $this->charge_mode; } /** * Assert money matches context currency. * * @param Money $money Money value. * * @throws DomainException On mismatch. */ private function assert_currency( Money $money ): void { if ( $money->currency() !== $this->currency ) { throw new DomainException( 'money.currency_mismatch', 'Cannot operate on Money values with different currencies.' ); } } }