diff --git a/_config.php b/_config.php index 23145e2..abcf37d 100644 --- a/_config.php +++ b/_config.php @@ -3,4 +3,6 @@ define('ECOMMERCE_COUPON_DIR','shop_discount'); Object::add_extension("CheckoutPage_Controller", "CouponFormCheckoutDecorator"); DataObject::add_extension("Product_OrderItem", "DiscountedOrderItem"); -DataObject::add_extension("CheckoutPage_Controller", "Product_OrderItem_Coupon"); \ No newline at end of file +DataObject::add_extension("CheckoutPage_Controller", "Product_OrderItem_Coupon"); + +SS_Report::register('ReportAdmin', 'CouponReport',3); \ No newline at end of file diff --git a/code/CheckoutStep_Discount.php b/code/CheckoutStep_Discount.php new file mode 100644 index 0000000..413738e --- /dev/null +++ b/code/CheckoutStep_Discount.php @@ -0,0 +1,9 @@ +owner,"CouponForm"); + } + +} \ No newline at end of file diff --git a/code/cms/CouponReport.php b/code/cms/CouponReport.php new file mode 100644 index 0000000..2634d3a --- /dev/null +++ b/code/cms/CouponReport.php @@ -0,0 +1,57 @@ + "Title", + "Code" => "Code", + "DiscountNice" => "Discount", + "Uses" => "Uses", + "Savings" => "Savings" + ); + } + + /** + * Remove unsortable columns + */ + function sortColumns() { + $cols = parent::sortColumns(); + unset($cols['DiscountNice']); + return $cols; + } + + function getReportField(){ + $field = parent::getReportField(); + $field->addSummary("Total",array( + "Uses"=>"sum", + "Savings"=> array("sum","Currency->Nice") + )); + return $field; + } + + function query($params){ + $query = parent::query($params); + $query->select( + "$this->periodfield AS FilterPeriod", + "OrderCoupon.*", + "\"Title\" AS \"Name\"", + "COUNT(OrderCouponModifier.ID) AS Uses", + "SUM(OrderModifier.Amount) AS Savings"); + $query->innerJoin("OrderCouponModifier", "OrderCoupon.ID = OrderCouponModifier.CouponID"); + $query->innerJoin("OrderAttribute", "OrderCouponModifier.ID = OrderAttribute.ID"); + $query->innerJoin("OrderModifier", "OrderCouponModifier.ID = OrderModifier.ID"); + $query->innerJoin("Order", "OrderAttribute.OrderID = Order.ID"); + $query->groupby("OrderCoupon.ID"); + if(!$query->orderby){ + $query->orderby("Uses DESC,Title ASC"); + } + $query->limit("50"); + return $query; + } + +} \ No newline at end of file diff --git a/code/cms/CouponsModelAdmin.php b/code/cms/CouponsModelAdmin.php index b8870a4..4944ad1 100644 --- a/code/cms/CouponsModelAdmin.php +++ b/code/cms/CouponsModelAdmin.php @@ -94,7 +94,6 @@ function ResultsForm($searchCriteria){ } return $form; } - } @@ -102,5 +101,5 @@ function ResultsForm($searchCriteria){ * @package shop-discount */ class CouponsModelAdmin_RecordController extends ModelAdmin_RecordController{ - + } diff --git a/code/forms/CouponDatetimeField.php b/code/forms/CouponDatetimeField.php new file mode 100644 index 0000000..a7503da --- /dev/null +++ b/code/forms/CouponDatetimeField.php @@ -0,0 +1,18 @@ +dateField->setConfig("showcalendar", true); + + } + + function setValue($val){ + if($val == array('date' => '', 'time' => '')){ + $val = null; + } + parent::setValue($val); + } + +} \ No newline at end of file diff --git a/code/CouponForm.php b/code/forms/CouponForm.php similarity index 82% rename from code/CouponForm.php rename to code/forms/CouponForm.php index 5be02f7..378a3ee 100644 --- a/code/CouponForm.php +++ b/code/forms/CouponForm.php @@ -7,9 +7,9 @@ class CouponForm extends OrderModifierForm{ function __construct($controller = null, $name){ - $fields = new FieldSet(); - $fields->push(new HeaderField('CouponHeading',_t("CouponForm.COUPONHEADING", 'Coupon/Voucher Code'),3)); - $fields->push(new TextField('Code',_t("CouponForm.COUPON", 'Enter your coupon code if you have one.'))); + $fields = new FieldSet( + new TextField('Code',_t("CouponForm.COUPON", 'Enter your coupon code if you have one.')) + ); $actions = new FieldSet(new FormAction('apply', _t("CouponForm.APPLY", 'Apply'))); $validator = new CouponFormValidator(array('Code')); @@ -36,10 +36,8 @@ function apply($data,$form){ if(Director::is_ajax()) { return $messagetype; } - else { - $form->sessionMessage($message,$messagetype); - Director::redirect(CheckoutPage::find_link()); - } + $form->sessionMessage($message,$messagetype); + $this->Controller()->redirectBack(); return; } @@ -61,8 +59,8 @@ function php($data) { } //check the coupon exists, and can be used if($coupon = OrderCoupon::get_by_code($data['Code'])){ - if(!$coupon->valid($cart)){ - $this->validationError('Code',$coupon->validationerror,"bad"); + if(!$coupon->valid($order)){ + $this->validationError('Code',$coupon->getMessage(),"bad"); return false; } }else{ diff --git a/code/model/GiftVoucherProduct.php b/code/model/GiftVoucherProduct.php new file mode 100644 index 0000000..6f35fc9 --- /dev/null +++ b/code/model/GiftVoucherProduct.php @@ -0,0 +1,163 @@ + "Boolean", + "MinimumAmount" => "Currency" + ); + + static $order_item = "GiftVoucher_OrderItem"; + + public static $singular_name = "Gift Voucher"; + function i18n_singular_name() { return _t("GiftVoucherProduct.SINGULAR", $this->stat('singular_name')); } + public static $plural_name = "Gift Vouchers"; + function i18n_plural_name() { return _t("GiftVoucherProduct.PLURAL", $this->stat('plural_name')); } + + function getCMSFields(){ + $fields = parent::getCMSFields(); + $fields->addFieldToTab("Root.Content.Pricing", + new OptionsetField("VariableAmount","Price",array( + 0 => "Fixed", + 1 => "Allow customer to choose" + )), + "BasePrice" + ); + $fields->addFieldsToTab("Root.Content.Pricing", array( + $minimumamount = new TextField("MinimumAmount","Minimum Amount") //text field, because of CMS js validation issue + )); + $fields->removeByName("CostPrice"); + $fields->removeByName("Variations"); + $fields->removeByName("Model"); + return $fields; + } + + function canPurchase($member = null) { + if(!self::$global_allow_purchase) return false; + if(!$this->dbObject('AllowPurchase')->getValue()) return false; + if(!$this->isPublished()) return false; + return true; + } + +} + +class GiftVoucherProduct_Controller extends Product_Controller{ + + function Form(){ + $form = parent::Form(); + if($this->VariableAmount){ + $form->setSaveableFields(array( + "UnitPrice" + )); + $form->Fields()->push($giftamount = new CurrencyField("UnitPrice","Amount",$this->BasePrice)); //TODO: set minimum amount + $giftamount->setForm($form); + } + $form->setValidator($validator = new GiftVoucherFormValidator(array( + "Quantity", "UnitPrice" + ))); + return $form; + } + +} + +class GiftVoucherFormValidator extends RequiredFields{ + + function php($data){ + $valid = parent::php($data); + if($valid){ + $controller = $this->form->Controller(); + if($controller->VariableAmount){ + $giftvalue = $data['UnitPrice']; + if($controller->MinimumAmount > 0 && $giftvalue < $controller->MinimumAmount){ + $this->validationError("UnitPrice", "Gift value must be at least ".$controller->MinimumAmount); + return false; + } + if($giftvalue <= 0){ + $this->validationError("UnitPrice", "Gift value must be greater than 0"); + return false; + } + } + } + return $valid; + } + +} + +class GiftVoucher_OrderItem extends Product_OrderItem{ + + static $db = array( + "GiftedTo" => "Varchar" + ); + + static $has_many = array( + "Coupons" => "OrderCoupon" + ); + + static $required_fields = array( + "UnitPrice" + ); + + /** + * Don't get unit price from product + */ + function UnitPrice() { + if($this->Product()->VariableAmount){ + return $this->UnitPrice; + } + return parent::UnitPrice(); + } + + /** + * Create vouchers on order payment success event + */ + function onPayment(){ + parent::onPayment(); + if($this->Coupons()->Count() < $this->Quantity){ + $remaining = $this->Quantity - $this->Coupons()->Count(); + for($i = 0; $i < $remaining; $i++){ + if($coupon = $this->createCoupon()){ + $this->sendVoucher($coupon); + } + } + } + } + + /** + * Create a new coupon, based on this orderitem + * @return OrderCoupon + */ + function createCoupon(){ + if(!$this->Product()){ + return false; + } + $coupon = new OrderCoupon(array( + "Title" => $this->Product()->Title, + "Type" => "Amount", + "Amount" => $this->UnitPrice, + "UseLimit" => 1, + "MinOrderValue" => $this->UnitPrice //safeguard that means coupons must be used entirely + )); + $this->extend("updateCreateCupon",$coupon); + $coupon->write(); + $this->Coupons()->add($coupon); + return $coupon; + } + + /* + * Send the voucher to the appropriate email + */ + function sendVoucher(OrderCoupon $coupon){ + $from = Email::getAdminEmail(); + $to = $this->Order()->getLatestEmail(); + $subject = _t("Order.GIFTVOUCHERSUBJECT", "Gift voucher"); + $email = new Email($from, $to, $subject); + $email->setTemplate("GiftVoucherEmail"); + $email->populateTemplate(array( + 'Coupon' => $coupon + )); + return $email->send(); + } + +} \ No newline at end of file diff --git a/code/model/OrderCoupon.php b/code/model/OrderCoupon.php index f09709f..dda017b 100644 --- a/code/model/OrderCoupon.php +++ b/code/model/OrderCoupon.php @@ -1,5 +1,4 @@ "Varchar(255)", //store the promotion name, or whatever you like "Code" => "Varchar(25)", - //"Type" => "Enum('Voucher,GiftCard,Coupon','Coupon')", + "Type" => "Enum('Percent,Amount','Percent')", "Amount" => "Currency", - "Percent" => "Decimal(4,2)", - - //Restrictions + "Percent" => "Percentage", "Active" => "Boolean", - "StartDate" => "Datetime", - "EndDate" => "Datetime", - "UseLimit" => "Int", + + "ForItems" => "Boolean", + "ForShipping" => "Boolean", + + //Item / order validity criteria + //"Cumulative" => "Boolean", "MinOrderValue" => "Currency", - //per-order use limit + "UseLimit" => "Int", + "StartDate" => "Datetime", + "EndDate" => "Datetime" ); - static $defaults = array( - "Active" => true, - "UseLimit" => 1 + static $has_one = array( + "GiftVoucher" => "GiftVoucher_OrderItem", //used to link to gift voucher purchase + "Group" => "Group" ); - + static $many_many = array( - "Products" => "Product" //for restricting to product(s) + "Products" => "Product", //for restricting to product(s) + "Categories" => "ProductCategory", + "Zones" => "Zone" ); static $searchable_fields = array( "Code" ); + static $defaults = array( + "Type" => "Percent", + "Active" => true, + "UseLimit" => 0, + //"Cumulative" => 1, + "ForItems" => 1 + ); + static $field_labels = array( - "Amount" => "Amount", - "Percent" => "Percent", - "UseLimit" => "Usage Limit" + "DiscountNice" => "Discount", + "UseLimit" => "Maximum number of uses", + "MinOrderValue" => "Minimum subtotal of order" ); static $summary_fields = array( "Code", "Title", - "Amount", - "Percent", + "DiscountNice", "StartDate", "EndDate" ); @@ -56,7 +67,6 @@ function i18n_singular_name() { return _t("OrderCoupon.COUPON", "Coupon");} function i18n_plural_name() { return _t("OrderCoupon.COUPONS", "Coupons");} static $default_sort = "EndDate DESC, StartDate DESC"; - static $code_length = 10; static function get_by_code($code){ @@ -76,84 +86,231 @@ static function generateCode($length = null){ return $code; } + protected $message = null, $messagetype = null; + function getCMSFields($params = null){ - $fields = parent::getCMSFields($params); - $fields->removeByName("Products"); - if($this->ID){ - $products = new ManyManyComplexTableField($this, "Products", "Product"); - $fields->addFieldToTab("Root.Products", $products); + $fields = new FieldSet( + new TextField("Title"), + new TextField("Code"), + new CheckboxField("Active","Active (allow this coupon to be used)"), + new FieldGroup("This discount applies to:", + new CheckboxField("ForItems","Item values"), + new CheckboxField("ForShipping","Shipping cost") + ), + new HeaderField("Criteria","Order and Item Criteria",4), + new LabelField("CriteriaDescription", "Configure the requirements an order must meet for this coupon to be used with it:"), + $tabset = new TabSet("Root", + $maintab = new Tab("Main", + new FieldGroup("Valid date range:", + new CouponDatetimeField("StartDate","Start Date / Time"), + new CouponDatetimeField("EndDate","End Date / Time (you should set the end time to 23:59:59, if you want to include the entire end day)") + ), + new CurrencyField("MinOrderValue","Minimum order subtotal"), + new NumericField("UseLimit","Limit number of uses (0 = unlimited)") + ) + ) + ); + if($this->isInDB()){ + if($this->ForItems){ + $tabset->push(new Tab("Products", + new LabelField("ProductsDescription", "Select specific products that this coupon can be uesd with"), + $products = new ManyManyComplexTableField($this, "Products", "Product") + )); + $tabset->push(new Tab("Categories", + new LabelField("CategoriesDescription", "Select specific product categories that this coupon can be uesd with"), + $categories = new ManyManyComplexTableField($this, "Categories", "ProductCategory") + )); + $products->setPermissions(array('show')); + $categories->setPermissions(array('show')); + } + + $tabset->push(new Tab("Zones", + $zones = new ManyManyComplexTableField($this, "Zones", "Zone") + )); + + $maintab->Fields()->push($grps = new DropdownField("GroupID", "Member Belongs to Group", DataObject::get('Group')->map('ID','Title'))); + $grps->setHasEmptyDefault(true); + + if($this->Type == "Percent"){ + $fields->insertBefore($percent = new NumericField("Percent","Percentage discount"), "Active"); + $percent->setTitle("Percent discount (eg 0.05 = 5%, 0.5 = 50%, and 5 = 500%)"); + }elseif($this->Type == "Amount"){ + $fields->insertBefore($amount = new NumericField("Amount","Discount value"), "Active"); + } }else{ - $fields->addFieldToTab("Root.Main", new LiteralField("warning","

You can specify products this coupon applies to after you save.

")); + $fields->insertBefore( + new OptionsetField("Type","Type of discount", + array( + "Percent" => "Percentage of subtotal (eg 25%)", + "Amount" => "Fixed amount (eg $25.00)" + ) + ), + "Active" + ); + $fields->insertAfter( + new LiteralField("warning","

More criteria options can be set after an intial save

"), + "Criteria" + ); } - $fields->fieldByName("Root.Main.Percent")->setTitle("Percent (eg 0.5 = 50% and 5 = 500%)"); + $this->extend("updateCMSFields",$fields); return $fields; } function populateDefaults() { + parent::populateDefaults(); $this->Code = self::generateCode(); } + /* + * Assign this coupon to a OrderCouponModifier on the given order + */ function applyToOrder(Order $order){ - if($modifier = $order->getModifier('OrderCouponModifier',true)){ + $modifier = $order->getModifier('OrderCouponModifier',true); + if($modifier){ $modifier->setCoupon($this); $modifier->write(); $order->calculate(); //makes sure prices are up-to-date $order->write(); - //TODO: set message + $this->message(_t("OrderCoupon.APPLIED","Coupon applied."),"good"); return true; } - //TODO: get/set error message + $this->error(_t("OrderCoupon.CANTAPPLY","Could not apply")); return false; } /** - * Self check if the coupon can be used. + * Check if this coupon can be used with a given order * @return boolean */ - function valid($order = null){ + function valid($order){ + if(empty($order)){ + $this->error(_t("OrderCoupon.NOORDER","Order has not been started.")); + return false; + } if(!$this->Active){ - $this->validationerror = _t("OrderCoupon.INACTIVE","This coupon is not active."); + $this->error(_t("OrderCoupon.INACTIVE","This coupon is not active.")); return false; } - if($this->UseLimit > 0 && $this->getUseCount() < $this->UseLimit) { - $this->validationerror = _t("OrderCoupon.LIMITREACHED","Limit of $this->UseLimit uses for this code has been reached."); + if($this->UseLimit > 0 && $this->getUseCount($order) >= $this->UseLimit) { + $this->error(_t("OrderCoupon.LIMITREACHED","Limit of $this->UseLimit uses for this code has been reached.")); return false; } + if($this->MinOrderValue > 0 && $order->SubTotal() < $this->MinOrderValue){ + $this->error(sprintf(_t("OrderCouponModifier.MINORDERVALUE","Your cart subtotal must be at least %s to use this coupon"),$this->dbObject("MinOrderValue")->Nice())); + return false; + } $startDate = strtotime($this->StartDate); $endDate = strtotime($this->EndDate); $today = strtotime("today"); $yesterday = strtotime("yesterday"); if($endDate && $endDate < $yesterday){ - $this->validationerror = _t("OrderCoupon.EXPIRED","This coupon has already expired."); + $this->error(_t("OrderCoupon.EXPIRED","This coupon has already expired.")); return false; } if($startDate && $startDate > $today){ - $this->validationerror = _t("OrderCoupon.TOOEARLY","It is too early to use this coupon."); + $this->error(_t("OrderCoupon.TOOEARLY","It is too early to use this coupon.")); return false; } - - if($order){ - if($this->MinOrderValue && $order->SubTotal() < $this->MinOrderValue){ - $this->validationerror = sprintf(_t("OrderCouponModifier.MINORDERVALUE","The minimum order value has not been reached."),$this->MinOrderValue); + $group = $this->Group(); + $member = (Member::currentUser()) ? Member::currentUser() : $order->Member(); //get member + if($group->exists() && (!$member || !$member->inGroup($group))){ + $this->error(_t("OrderCoupon.GROUPED","Only specific members can use this coupon.")); + return false; + } + $zones = $this->Zones(); + if($zones->exists()){ + $address = $order->getShippingAddress(); + if(!$address){ + $this->error(_t("OrderCouponModifier.NOTINZONE","This coupon can only be used for a specific shipping location.")); + return false; + } + $currentzones = Zone::get_zones_for_address($address); + if(!$currentzones || !$currentzones->exists()){ + $this->error(_t("OrderCouponModifier.NOTINZONE","This coupon can only be used for a specific shipping location.")); return false; } - $products = $this->Products(); - if($products->exists()){ - $incart = false; - foreach($products as $product){ - if($order->Items()->find('ProductID',$product->ID)){ - $incart = true; - break; - } + //check if any of currentzones is in zones + $inzone = false; + foreach($currentzones as $zone){ + if($zones->find('ID',$zone->ID)){ + $inzone = true; + break; } - if(!$incart){ - $this->validationerror = _t("OrderCouponModifier.PRODUCTNOTINORDER","The required product is not in the order."); - return false; + } + if(!$inzone){ + $this->error(_t("OrderCouponModifier.NOTINZONE","This coupon can only be used for a specific shipping location.")); + return false; + } + } + $items = $order->Items(); + $incart = false; //note that this means an order without items will always be invalid + foreach($items as $item){ + if($this->itemMatchesCriteria($item)){ //check at least one item in the cart meets the coupon's criteria + $incart = true; + break; + } + } + if(!$incart){ + $this->error(_t("OrderCouponModifier.PRODUCTNOTINORDER","No items in the cart match the coupon criteria")); + return false; + } + $valid = true; + $this->extend("updateValidation",$order, $valid, $error); + if(!$valid){ + $this->error($error); + } + return $valid; + } + + /** + * Work out the discount for a given order. + * @return discount + */ + function orderDiscount(Order $order){ + $discount = 0; + if($this->ForItems){ + $items = $order->Items(); + $discountable = 0; + foreach($items as $item){ + if($this->itemMatchesCriteria($item)){ + $discountable += $item->Total(); } } - //TODO: limited number of uses + if($discountable){ + $discountvalue = $this->getDiscountValue($discountable); + $discount += ($discountvalue > $discountable) ? $discountable : $discountvalue; //prevent discount being greater than what is possible + } } - return true; + if($this->ForShipping){ + if($shipping = $order->getModifier("ShippingFrameworkModifier")){ + $discount += $shipping->Amount; + } + } + return $discount; + } + + /** + * Check if order item meets criteria of this coupon + * @param OrderItem $item + * @return boolean + */ + function itemMatchesCriteria(OrderItem $item){ + $products = $this->Products(); + if($products->exists()){ + if(!$products->find('ID', $item->ProductID)){ + return false; + } + } + $categories = $this->Categories(); + if($categories->exists()){ + $itemproduct = $item->Product(true); //true forces the current version of product to be retrieved. + if(!$itemproduct || !$categories->find('ID', $itemproduct->ParentID)){ + return false; + } + } + $match = true; + $this->extend("updateItemCriteria",$item, $match); + return $match; } /** @@ -172,14 +329,25 @@ function getDiscountValue($subTotal){ return $discount; } + function getDiscountNice(){ + if($this->Type == "Percent"){ + return $this->dbObject("Percent")->Nice(); + } + return $this->dbObject("Amount")->Nice(); + } + /** - * How many times the coupon has been used. + * How many times the coupon has been used + * @param string $order - ignore this order when counting uses * @return int */ - function getUseCount() { - $objects = DataObject::get("OrderCouponModifier", "\"CouponID\" = ".$this->ID); - if($objects) { - return $objects->count(); + function getUseCount($order = null) { + $currentorderfilter = ""; + if($order){ + $currentorderfilter = " AND \"OrderID\" != ".$order->ID; + } + if($usedcoupons = DataObject::get("OrderCouponModifier", "\"CouponID\" = ".$this->ID.$currentorderfilter)) { + return $usedcoupons->Count(); } return 0; } @@ -193,7 +361,6 @@ function setCode($code){ $this->setField("Code", strtoupper($code)); } - function canDelete($member = null) { if($this->getUseCount()) { return false; @@ -207,5 +374,22 @@ function canEdit($member = null) { } return true; } + + protected function message($messsage, $type = "good"){ + $this->message = $messsage; + $this->messagetype = $type; + } + + protected function error($message){ + $this->message($message, "bad"); + } + + function getMessage(){ + return $this->message; + } + + function getMessageType(){ + return $this->messagetype; + } } \ No newline at end of file diff --git a/code/modifiers/OrderCouponModifier.php b/code/modifiers/OrderCouponModifier.php index b822475..63eb709 100644 --- a/code/modifiers/OrderCouponModifier.php +++ b/code/modifiers/OrderCouponModifier.php @@ -53,21 +53,7 @@ public function canRemove() { */ function value($incoming){ if($coupon = $this->Coupon()){ - $discount = 0; - $products = $coupon->Products(); - if($products->exists()){ - $cart = ShoppingCart::getInstance(); - foreach($products as $product){ - if($item = $cart->get($product)){ - for($i = 0; $i < $item->Quantity; $i++){ - $discount += $coupon->getDiscountValue($item->UnitPrice()); - } - } - } - }else{ - $discount = $coupon->getDiscountValue($incoming); - } - $this->Amount = $discount; + $this->Amount = $coupon->orderDiscount($this->Order()); } return $this->Amount; } diff --git a/docs/en/index.md b/docs/en/index.md index b018098..19e810e 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -5,7 +5,6 @@ Discounts come in two forms: * Temporary price markdown on one or many products - displayed on front-end, and may vary, depending on logged in member * Coupon/Voucher codes - unique code entered at checkout - ## Vouchers ### Setup Options @@ -16,6 +15,12 @@ Discounts come in two forms: * Usage limit - unlimited, or a certian number of uses * Use period - specify a start / end period that the voucher can be used within. * Product range - restrict to specific products. + +### How it works + +When the user enters a cupon at the checkout, the coupon will check that the given order matches +its criteria. First global criteria (like the current date) will be checked, and then it will +check that at least one item in the cart matches the item citeria (eg product is from particular category). ### Common use cases @@ -26,4 +31,3 @@ Discounts come in two forms: ### System Restrictions * Multiple coupons cannot be used for a single product. - diff --git a/templates/email/GiftVoucherEmail.ss b/templates/email/GiftVoucherEmail.ss new file mode 100644 index 0000000..57ad07a --- /dev/null +++ b/templates/email/GiftVoucherEmail.ss @@ -0,0 +1,16 @@ +

Here is your gift voucher

+ +<% control Coupon %> +
+

Code: $Code

+ <% if Type = Percent %> +

Percent: $Percent.Nice

+ <% else %> +

Amount: $Amount.Nice

+ <% end_if %> + + <% if EndDate %> +

This voucher must be used by $EndDate.Long

+ <% end_if %> +
+<% end_control %> \ No newline at end of file diff --git a/tests/CouponModifiersTest.php b/tests/CouponModifiersTest.php index 5be7793..0ab1812 100644 --- a/tests/CouponModifiersTest.php +++ b/tests/CouponModifiersTest.php @@ -3,7 +3,7 @@ class CouponModifiersTest extends SapphireTest{ static $fixture_file = array( - 'shop_discount/tests/OrderCoupons.yml', + 'shop_discount/tests/fixtures/OrderCoupons.yml', 'shop/tests/fixtures/Cart.yml' ); @@ -25,11 +25,11 @@ function testPlaceDiscountedOrder(){ $order = $this->objFromFixture("Order", "cart1"); $order->calculate(); $this->assertEquals($order->GrandTotal(), 2000, "Price without coupon is $2000"); - $coupon = $this->objFromFixture("OrderCoupon", "validcoupon"); + $coupon = $this->objFromFixture("OrderCoupon", "50percentoff"); $valid = $coupon->valid($order); - $this->assertTrue($valid,'Check the coupon is indeed valid'); + $this->assertTrue($valid,'Check the coupon is indeed valid '.$coupon->getMessage()); $coupon->applyToOrder($order); - $this->assertEquals($order->GrandTotal(), 998, "Half price & -$2 coupon"); + $this->assertEquals($order->GrandTotal(), 1000, "Half price"); } //TODO: test product discounts diff --git a/tests/CouponTest.php b/tests/CouponTest.php deleted file mode 100644 index 7380872..0000000 --- a/tests/CouponTest.php +++ /dev/null @@ -1,34 +0,0 @@ -objFromFixture('OrderCoupon', 'validcoupon'); - $this->assertTrue($validcoupon->valid()); - $this->assertEquals($validcoupon->getDiscountValue(10),7); - } - - function testExpiredCoupon(){ - $expiredcoupon = $this->objFromFixture('OrderCoupon', 'expiredcoupon'); - $this->assertFalse($expiredcoupon->valid()); - #TODO: check error - } - - function testUnreleasedCoupon(){ - $expiredcoupon = $this->objFromFixture('OrderCoupon', 'unreleasedcoupon'); - $this->assertFalse($expiredcoupon->valid()); - #TODO: check error - } - - function testInactiveCoupon(){ - $inactivecoupon = $this->objFromFixture('OrderCoupon', 'inactivecoupon'); - $this->assertFalse($inactivecoupon->valid()); - #TODO: check error - } - -} \ No newline at end of file diff --git a/tests/GiftVoucherTest.php b/tests/GiftVoucherTest.php new file mode 100644 index 0000000..387b7da --- /dev/null +++ b/tests/GiftVoucherTest.php @@ -0,0 +1,67 @@ +variable = $this->objFromFixture("GiftVoucherProduct", "variable"); + $this->variable->publish('Stage','Live'); + + $this->fixed10 = $this->objFromFixture("GiftVoucherProduct", "10fixed"); + $this->fixed10->publish('Stage','Live'); + } + + function testCusomisableVoucher(){ + + $controller = new GiftVoucherProduct_Controller($this->variable); + $form = $controller->Form(); + + $form->loadDataFrom($data = array( + "UnitPrice" => 32.35, + "Quantity" => 1 + )); + $this->assertTrue($form->validate(), "Voucher form is valid"); + + $form->loadDataFrom(array( + "UnitPrice" => 3, + "Quantity" => 5 + )); + $this->assertFalse($form->validate(), "Tested unit price is below minimum amount"); + + $form->loadDataFrom(array( + "UnitPrice" => 0, + "Quantity" => 5 + )); + $this->assertFalse($form->validate(), "Tested unit price is zero"); + } + + function testFixedVoucher(){ + $controller = new GiftVoucherProduct_Controller($this->fixed10); + $form = $controller->Form(); + $form->loadDataFrom(array( + "Quantity" => 2 + )); + $this->assertTrue($form->validate(), "Valid voucher"); + } + + function testCreateCoupon(){ + $item = $this->variable->createItem(1,array( + "UnitPrice" => 15.00 + )); + $coupon = $item->createCoupon(); + $this->assertEquals($coupon->Amount,15,"Coupon value is $15, as per order item"); + $this->assertEquals($coupon->Type,"Amount","Coupon type is 'Amount'"); + } + + function testOnPayment(){ + + } + + //TODO: ensure gift vouchers can only be used once + +} \ No newline at end of file diff --git a/tests/OrderCouponTest.php b/tests/OrderCouponTest.php new file mode 100644 index 0000000..99bc223 --- /dev/null +++ b/tests/OrderCouponTest.php @@ -0,0 +1,128 @@ +placedorder = $this->objFromFixture("Order", "placed"); + $this->cart = $this->objFromFixture("Order", "cart"); + $this->othercart = $this->objFromFixture("Order", "othercart"); + } + + function testPercent(){ + $coupon = $this->objFromFixture('OrderCoupon', '50percentoff'); + $this->assertTrue($coupon->valid($this->cart)); + $this->assertEquals($coupon->getDiscountValue(10), 5, "50% off value"); + $this->assertEquals($coupon->orderDiscount($this->placedorder), 250, "50% off order"); + } + + function testAmount(){ + $coupon = $this->objFromFixture('OrderCoupon', '10dollarsoff'); + $this->assertTrue($coupon->valid($this->cart)); + $this->assertEquals($coupon->getDiscountValue(1000), 10, "$10 off fixed value"); + $this->assertEquals($coupon->orderDiscount($this->placedorder), 10, "$10 off order"); + //TODO: test ammount that is greater than order value + } + + function testProductsDiscount(){ + $coupon = $this->objFromFixture("OrderCoupon","products20percentoff"); + $coupon->Products()->add($this->objFromFixture("Product", "tshirt")); //add product to coupon product list + $this->assertEquals($coupon->orderDiscount($this->placedorder), 20); + $coupon->Products()->add($this->objFromFixture("Product", "mp3player")); //add another product to coupon product list + $this->assertEquals($coupon->orderDiscount($this->placedorder), 100); + } + + function testCategoryDiscount(){ + $coupon = $this->objFromFixture("OrderCoupon","clothing5percent"); + $coupon->Categories()->add($this->objFromFixture("ProductCategory", "clothing")); + $this->socks = $this->objFromFixture("Product", "socks"); + $this->socks->publish('Stage','Live'); + $this->assertTrue($coupon->valid($this->cart), "Order contains a t-shirt. ".$coupon->getMessage()); + $this->assertEquals($coupon->orderDiscount($this->cart), 0.4,"5% discount for socks in cart"); + $this->assertFalse($coupon->valid($this->othercart),"Order does not contain clothing"); + $this->assertEquals($coupon->orderDiscount($this->othercart), 0, "No discount, because no product in category"); + } + + function testZoneDiscount(){ + $coupon = $this->objFromFixture('OrderCoupon', 'zoned'); + //add zones to coupon + $coupon->Zones()->add($this->objFromFixture('Zone', 'transtasman')); + $coupon->Zones()->add($this->objFromFixture('Zone', 'special')); + $address = $this->objFromFixture("Address", 'bukhp193eq'); + $this->cart->ShippingAddressID = $address->ID; //set address + $this->assertFalse($coupon->valid($this->cart),"check order is out of zone"); + $address = $this->objFromFixture("Address", 'sau5024'); + $this->othercart->ShippingAddressID = $address->ID; //set address + $valid = $coupon->valid($this->othercart); + $this->assertTrue($valid,"check order is in zone"); + } + + function testMinOrderValue(){ + $coupon = $this->objFromFixture("OrderCoupon", "ordersabove200"); + $this->assertFalse($coupon->valid($this->cart),"$8 order isn't enough"); + $this->assertTrue($coupon->valid($this->placedorder),"$500 order is enough"); + } + + function testUseLimit(){ + $coupon = $this->objFromFixture("OrderCoupon", "used"); + $this->assertFalse($coupon->valid($this->cart),"Coupon is already used"); + $coupon = $this->objFromFixture("OrderCoupon", "limited"); + $this->assertTrue($coupon->valid($this->cart),"Coupon has been used, but can continue to be used"); + } + + function testMemberGroup(){ + $group = $this->objFromFixture("Group","resellers"); + $coupon = $this->objFromFixture("OrderCoupon", "grouped"); + $coupon->GroupID = $group->ID; + $this->assertFalse($coupon->valid($this->cart),"Invalid for memberless order"); + $this->assertTrue($coupon->valid($this->othercart),"Valid because member is in resellers group"); + } + + function testInactiveCoupon(){ + $inactivecoupon = $this->objFromFixture('OrderCoupon', 'inactivecoupon'); + $this->assertFalse($inactivecoupon->valid($this->cart),"Coupon is not set to active"); + } + + function testDates(){ + $expiredcoupon = $this->objFromFixture('OrderCoupon', 'unreleasedcoupon'); + $this->assertFalse($expiredcoupon->valid($this->cart),"Coupon is un released (start date has not arrived)"); + $expiredcoupon = $this->objFromFixture('OrderCoupon', 'expiredcoupon'); + $this->assertFalse($expiredcoupon->valid($this->cart),"Coupon has expired (end date has passed)"); + } + + function testFreeShipping(){ + $coupon = $this->objFromFixture("OrderCoupon", "freeshipping"); + $order = $this->cart; + $shipping = new ShippingFrameworkModifier(array( + 'Amount' => 12.34, + 'OrderID' => $order->ID + )); + $shipping->write(); + $order->Modifiers()->add($shipping); + $this->assertEquals($coupon->orderDiscount($order), 12.34, "Shipping discount"); + } + + function testCumulative(){ + $order = $this->cart; + //$coupon->applyToOrder($order); + //add coupon + //check that it remains + //add non-conflicting + //check that both remain + //add conflicting coupon + //check that only conflicting coupon exists + //check message given + } + +} \ No newline at end of file diff --git a/tests/OrderCoupons.yml b/tests/OrderCoupons.yml deleted file mode 100644 index 3b3e487..0000000 --- a/tests/OrderCoupons.yml +++ /dev/null @@ -1,26 +0,0 @@ -OrderCoupon: - validcoupon: - Title: 50% + $2 off coupon - Code: 5B97AA9D75 - Amount: 2 - Percent: 0.50 - Active: 1 - StartDate: 2000-01-01 12:00:00 - EndDate: 2200-01-01 12:00:00 - unreleasedcoupon: - Title: Unreleased $10 off - Code: 0444444440 - Amount: 10 - StartDate: 2200-01-01 12:00:00 - expiredcoupon: - Title: Save lots - Code: 04994C332A - Percent: 0.8 - Active: 1 - StartDate: - EndDate: 12/12/1990 - inactivecoupon: - Title: Not active - Code: EE891574D6 - Amount: 10 - Active: 0 \ No newline at end of file diff --git a/tests/fixtures/GiftVouchers.yml b/tests/fixtures/GiftVouchers.yml new file mode 100644 index 0000000..702e99e --- /dev/null +++ b/tests/fixtures/GiftVouchers.yml @@ -0,0 +1,14 @@ +GiftVoucherProduct: + variable: + Title: Variable Gift Voucher + BasePrice: 0 + VariableAmount: 1 + MinimumAmount: 10 + 25fixed: + Title: $25 gift voucher + BasePrice: 25 + VariableAmount: 0 + 10fixed: + Title: $10 gift voucher + BasePrice: 10 + VariableAmount: 0 \ No newline at end of file diff --git a/tests/fixtures/OrderCoupons.yml b/tests/fixtures/OrderCoupons.yml new file mode 100644 index 0000000..ec495a2 --- /dev/null +++ b/tests/fixtures/OrderCoupons.yml @@ -0,0 +1,92 @@ +OrderCoupon: + 50percentoff: + Title: 50% off + Code: 5B97AA9D75 + Type: Percent + Percent: 0.50 + Active: 1 + StartDate: 2000-01-01 12:00:00 + EndDate: 2200-01-01 12:00:00 + 10dollarsoff: + Title: $10 off + Code: TENDOLLARSOFF + Type: Amount + Amount: 10 + Active: 1 + ordersabove200: + Title: $10 off + Code: TENDOLLARSOFF + Type: Amount + Amount: 10 + Active: 1 + MinOrderValue: 200 + products20percentoff: + Title: Selected products + Code: PRODUCTS + Percent: 0.2 + Active: 1 + clothing5percent: + Title: 5% off clothing + Code: CHEAPCLOTHING + Type: Percent + Percent: 0.05 + Active: 1 + zoned: + Title: Zoned Coupon + Code: ZONED + Type: Percent + Percent: 0.16 + Active: 1 + grouped: + Title: Special Members Coupon + Code: GROUPED + Type: Percent + Percent: 0.9 + Active: 1 + unreleasedcoupon: + Title: Unreleased $10 off + Code: 0444444440 + Type: Amount + Amount: 10 + StartDate: 2200-01-01 12:00:00 + expiredcoupon: + Title: Save lots + Code: 04994C332A + Type: Percent + Percent: 0.8 + Active: 1 + StartDate: + EndDate: 12/12/1990 + inactivecoupon: + Title: Not active + Code: EE891574D6 + Type: Amount + Amount: 10 + Active: 0 + noncumulative: + Title: Non cumulative + Code: NONCUMULATIVE023 + Type: Percent + Percent: 0.1 + Active: 1 + freeshipping: + Title: Free shipping + Code: FREESHIPPING + ForShipping: 1 + Active: 1 + limited: + Title: Limited + Code: LIMITED + Active: 1 + UseLimit: 10 + used: + Title: Used + Code: USEDCOUPON + Active: 1 + UseLimit: 1 + +OrderCouponModifier: + limited: + CouponID: =>OrderCoupon.limited + used: + CouponID: =>OrderCoupon.used \ No newline at end of file