How should I design this Registration System

Background

The company that I work for sells a review course for 2 different medical field licensure exams. For each course, CourseA and CourseB, we offer a Live, in-person version and an Online only version.

We do not use an e-commerce package because the they way our products work is so specialized that using an existing wasn’t feasible, and would be overkill.

The system, as it was originally built wasn’t not intended to have a person registering for more than one version of our course. However, we are finding that many, many of them want to purchase the Live and Online versions together. In order to facilitate this, we are implementing a discount/package system that would give customers a discount for purchasing both. Most of the unique stuff does come into play more when it is a registration for an existing customer than if is a brand new customer

Currently, the system looks like this:

abstract class Registration{}
class CourseALiveRegistration extends Registration{}
class CourseAOnlineRegistration extends Registration{}
class CourseBLiveRegistration extends CourseALiveRegistration{}
class CourseBOnlineRegistration extends CourseAOnlineRegistration{}

The CourseB classes extend the equivalent CourseA classes because each of their requirements are the same, but just things like price, subscription length, and a few other data values differ based on whether the registration is for CourseA or CourseB (For example, CourseA gets access to some content for 1 year while CourseB gets access for only 6 months to the same content).

Right now the registration process uses one of these registration classes to do its work. What I’d like some advice on is how to architect this system so that if, for example I register for both the Live and Online version of CourseA I would get a discount.

The reason for having multiple registration classes is because each type of registration CourseALive, CourseAOnline, CourseBLive, CourseBOnline have different sets of rules that control them. If the only differences were the actual things like cost, I could use just one class. However each has some unique business logic associated with it so each is a unique class to encapsulate the unique functionality. For example, if a customer has already purchase CourseBLive, they are able to purchase CourseALive for a discount (CourseB is a lower level license exam).

First, I think you complicated the system with multiple classes for multiple registration systems.
In case you will get course C you have to create another class. This is not a good OOP practice.

Here it is my idea:


<?php

class Registration {
	
	protected $COURSES = null;
	protected $DISCOUNTS = null;
	protected $CURRENT = array();
	
	public $price = 0;
	
	public function __construct() {
		
		// --- Get courses from database
		$courses = DB::getFromDatabaseAllCourses();
		
		// --- OR hard-coded - it's better from database
		// however, now you should have something like
		$this->COURSE = array(
			'a' => array (
				'name' => 'Course A',
				'supportedTypes' => array (
					'online' => 120, // type => price
					'live' => 190, // type => price
				),
			),
			'b' => array (
				'name' => 'Course B',
				'supportedTypes' => array (
					'online' => 220, // type => price
					'live' => 390, // type => price
				),
			),
		);
		
		
		// now, another important thing is "DISCOUNTS"
		// in case you need to keep your structure, just use $this->DISCOUNTS and $this->calculatePrice
		$this->DISCOUNTS = array(
			array(
				'components' => array(
					'a' => ( 'online', 'live' ),
				),
				'value' => 30,
			),
			array(
				'components' => array(
					'a' => ( 'online', 'live' ),
					'b' => ( 'live' ),
				),
				'value' => 20,
			),
			array(
				'components' => array(
					'a' => ( 'online', 'live' ),
					'b' => ( 'online' ),
				),
				'value' => 10,
			),
			// you get the point
		);
		
	}
	
	
	public function registerCourse( $course, $types = array() ) {
		//  // check if $types is like your accepted $this->COURSE[$course]
		$this->CURRENT[ $course ] = empty( $types ) ? $this->COURSE[$course]['supportedTypes'] : $this->validateTypes( $course, $types );
		$this->calculatePrice();
	}
	
	
	protected function validateTypes( $course, $types ) {
		// this will return:
		// array( 'online' => 120 ) // type => price
		// or throw some exception in case of error
	}
	
	
	// this is where you apply discounts
	// !!! this is what you need
	public function calculatePrice() {
	
		$price = 0;
		foreach( $this->CURRENT as $course => $typesPrices ) {
			// get the total price
			foreach( $typesPrices as $p ) {
				$price += $p;
			}
		}
		
		// apply only one discount in case your "cart"
		// is the same as one of the 'components' from $this->DISCOUNTS
		$DISCOUNT = $this->weHaveDiscount();
		if( $DISCOUNT ) {
			$newPrice = $price - ( $price * $DISCOUNT / 100 );
		}
		
		return array(
			'price' => $DISCOUNT > 0 ? $newPrice : $price,
			'fullPrice' => $DISCOUNT > 0 ? $price : 0,
		);
		
	}
	
	
	// apply only one discount in case your "cart"
	// is the same as one of the 'components' from $this->DISCOUNTS
	public function weHaveDiscount() {
		// just make few array operations
		// to check if you have any discount
	}
	
	
	// that's the main method
	public function run( $regDetails ) {
		// process your $regDetails -> name, email whatever
		// do all stuff
	}
	
	
}


// usage

try {
	$reg = new Registration;
	// register for all courses
	$reg->registerCourse( 'A', array('live') );
	$reg->registerCourse( 'B' ); // both
	$reg->run( $_POST );
} catch( Exception $e ) {
	// handle errors
	// $e->getMessage();
}

?>