*/ private array $modifiers; /** * Constructor. * * @param list|null $modifiers Optional override chain (tests / extensions). * @param CouponValidator|null $validator Coupon validator for the default coupon step. */ public function __construct( ?array $modifiers = null, ?CouponValidator $validator = null, ) { if ( null === $modifiers ) { $coupon_discount = new CouponDiscount( $validator ?? new CouponValidator() ); $modifiers = [ new BaseServicePrice(), new AgentPriceOverride(), new ExtrasPrice(), new GroupBaseFee(), $coupon_discount, new TaxCalculation(), new DepositCalculation(), ]; } usort( $modifiers, static fn( PriceModifier $a, PriceModifier $b ): int => $a->priority() <=> $b->priority() ); $this->modifiers = array_values( $modifiers ); } /** * Calculate order totals for the context. * * @param PriceContext $ctx Pricing inputs. * @return Result * * @since 0.1.0 */ public function calculate( PriceContext $ctx ): Result { try { return Result::ok( $this->calculate_or_throw( $ctx ) ); } catch ( DomainException $e ) { return Result::err( $e->code(), $e->getMessage() ); } } /** * Calculate or throw on domain failure. * * @param PriceContext $ctx Pricing inputs. * * @throws DomainException On currency mismatch or invalid coupon. * * @since 0.1.0 */ public function calculate_or_throw( PriceContext $ctx ): OrderTotals { $running = Money::zero( $ctx->currency() ); $subtotal = null; $discount = Money::zero( $ctx->currency() ); $tax = Money::zero( $ctx->currency() ); foreach ( $this->modifiers as $modifier ) { if ( $modifier instanceof DepositCalculation ) { if ( null === $subtotal ) { $subtotal = $running; } $total = $running; $due_now = $modifier->apply( $ctx, $total ); $due_later = $total->subtract( $due_now ); return new OrderTotals( $subtotal, $discount, $tax, $total, $due_now, $due_later ); } if ( $modifier instanceof CouponDiscount ) { $subtotal = $running; $discount = $modifier->discount_amount( $ctx, $running ); $running = $modifier->apply( $ctx, $running ); continue; } if ( $modifier instanceof TaxCalculation ) { if ( null === $subtotal ) { $subtotal = $running; } $tax = $modifier->tax_amount( $ctx, $running ); $running = $modifier->apply( $ctx, $running ); continue; } $running = $modifier->apply( $ctx, $running ); } if ( null === $subtotal ) { $subtotal = $running; } $total = $running; $due_now = $total; $due_later = Money::zero( $ctx->currency() ); return new OrderTotals( $subtotal, $discount, $tax, $total, $due_now, $due_later ); } }