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"," "), + "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 %> +