pos/is4c-nf/lib/Scanning/SpecialUPCs/DatabarCoupon.php
<?php
/*******************************************************************************
Copyright 2010 Whole Foods Co-op
This file is part of IT CORE.
IT CORE is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
IT CORE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
in the file license.txt along with IT CORE; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*********************************************************************************/
namespace COREPOS\pos\lib\Scanning\SpecialUPCs;
use COREPOS\pos\lib\Scanning\SpecialUPC;
use COREPOS\pos\lib\Database;
use COREPOS\pos\lib\DisplayLib;
use COREPOS\pos\lib\MiscLib;
use COREPOS\pos\lib\TransRecord;
use \stdClass;
class DatabarCoupon extends SpecialUPC
{
protected $offerCode = '';
public function isSpecial($upc)
{
if (substr($upc,0,4) == "8110" && strlen($upc) > 13) {
return true;
}
return false;
}
/**
Coupon information is parsed into a large object reflecting all the various
potential properties. These properties are:
* firstReq - An object representing a set of purchase requirements
* secondReq - An object representing a set of purchase requirements
* thirdReq - An object representing a set of purchase requirements
* offerCode - An identifier string [probably] unique to this coupon
* redeemValue - A baseline value, in cents, for simple coupons
* requiredRulesCode - integer value indicating which combination of the
first, second, and third requirements must be met before using the coupon
* dupePrefixFlag - boolean flag. When set to true, each requirement's family
code is appended to the requirement's prefix. This causes each requirement
to look for a specific subset of items from the manufacturer prefix.
* serial - Another embedded identifier. Not used for anything
* retailer - Another embedded identifier. Not used for anything
* valueCode - integer value indicating how the coupon information should
be translated into a dollar value when redeeming the coupon
* valueApplies - integer value indicating whether the value given in
the first, second, or third requirement object should be used when
determining final redemption dollar value
* storeCoupon - integer flag. Not used for anything
* noMultiply - integer flag. Not used for anything
The requirement object mentioned above has the following properties:
* valid - boolean indicating the requirement is met. This is the ONLY property
that's guaranteed to exist for all requirement objects. All other fields are
only populated if present in the coupon.
* prefix - string manufacturer barcode prefix
* code - integer indicating how to calculate the requirement's dollar value
* value - integer value used in calculating the requirement's dollar value
* family - string family code associated with the requirement
* price - retail price of an item in the transaction that meets this requirement
* department - POS department of an item in the transaction that meets this requirement
*/
public function handle($upc,$json)
{
$pos = 0;
$coupon = new stdClass();
$coupon->firstReq = new stdClass();
/* STEP 1 - REQUIRED FIELDS */
// remove prefix 8110
$pos += 4;
// grab company prefix length, remove it from barcode
$prefixLength = ((int)$upc[$pos]) + 6;
$pos += 1;
// grab company prefix, remove from barcode
$coupon->firstReq->prefix = substr($upc,$pos,$prefixLength);
$pos += $prefixLength;
// this way all prefixes map against
// localtemptrans.upc[2,length]
if ($prefixLength==6) {
$coupon->firstReq->prefix = '0' . $coupon->firstReq->prefix;
}
// grab offer code, remove from barcode
$offer = substr($upc,$pos,6);
$coupon->offerCode = substr($upc,$pos,6);
$this->offerCode = $offer;
$pos += 6;
// read value length
$valLength = (int)$upc[$pos];
$pos += 1;
// read value
$coupon->redeemValue = (int)substr($upc,$pos,$valLength);
$pos += $valLength;
// read primary requirement length
$reqLength = (int)$upc[$pos];
$pos += 1;
// read primary requirement value
$coupon->firstReq->value = substr($upc,$pos,$reqLength);
$pos += $reqLength;
// read primary requirement type-code
$coupon->firstReq->code = $upc[$pos];
$pos += 1;
// read primary requirement family code
$coupon->firstReq->family = substr($upc,$pos,3);
$pos += 3;
/* END REQUIRED FIELDS */
/* example:
barcode: 8110100707340143853100110110
company prefix length => 1 (+6)
company prefix => 0070734
offer code => 014385
value length => 3
value => 100
primary req length => 1
primary req value => 1
primary req code => 0
primary req family => 110
*/
/* STEP 2 - CHECK FOR OPTIONAL FIELDS */
// second required item
$coupon->secondReq = new stdClass();
$coupon->requiredRulesCode = 1;
$coupon->dupePrefixFlag = false;
if (isset($upc[$pos]) && $upc[$pos] == "1") {
$pos += 1;
$coupon->requiredRulesCode = $upc[$pos];
/**
* I cannot find any clear specification of what this rule is supposed to do.
* All real-life coupons seem to use it identically to rule #0
*/
if ($coupon->requiredRulesCode == 3) {
$coupon->requiredRulesCode = 0;
}
$pos += 1;
$srLength = (int)$upc[$pos];
$pos += 1;
$coupon->secondReq->value = substr($upc,$pos,$srLength);
$pos += $srLength;
$coupon->secondReq->code = $upc[$pos];
$pos += 1;
$coupon->secondReq->family = substr($upc,$pos,3);
$pos += 3;
$smLength = ((int)$upc[$pos]) + 6;
$pos += 1;
if ($smLength == 15) { // 9+6
$coupon->secondReq->prefix = $coupon->firstReq->prefix;
$coupon->dupePrefixFlag = true;
} else {
$coupon->secondReq->prefix = substr($upc,$pos,$smLength);
$pos += $smLength;
if ($smLength == 6) {
$coupon->secondReq->prefix = '0' . $coupon->secondReq->prefix;
}
}
}
// third required item
$coupon->thirdReq = new stdClass();
if (isset($upc[$pos]) && $upc[$pos] == "2") {
$pos += 1;
$trLength = (int)$upc[$pos];
$pos += 1;
$coupon->thirdReq->value = substr($upc,$pos,$trLength);
$pos += $trLength;
$coupon->thirdReq->code = $upc[$pos];
$pos += 1;
$coupon->thirdReq->family = substr($upc,$pos,3);
$pos += 3;
$tmLength = ((int)$upc[$pos]) + 6;
$pos += 1;
if ($tmLength == 15) { // 9+6
$coupon->thirdReq->prefix = $coupon->firstReq->prefix;
$coupon->dupePrefixFlag = true;
} else {
$coupon->thirdReq->prefix = substr($upc,$pos,$tmLength);
$pos += $tmLength;
if ($tmLength == 6) {
$coupon->thirdReq->prefix = '0' . $coupon->thirdReq->prefix;
}
}
}
if ($coupon->dupePrefixFlag) {
$coupon->firstReq->prefix .= $coupon->firstReq->family;
$coupon->secondReq->prefix .= $coupon->secondReq->family;
$coupon->thirdReq->prefix .= $coupon->thirdReq->family;
}
// expiration date
if (isset($upc[$pos]) && $upc[$pos] == "3") {
$pos += 1;
$expires = substr($upc,$pos,6);
$pos += 6;
$year = "20".substr($expires,0,2);
$month = substr($expires,2,2);
$day = substr($expires,4,2);
$tstamp = mktime(23,59,59,$month,$day,$year);
if ($tstamp < time()) {
$json['output'] = DisplayLib::boxMsg(
_("Coupon expired ") . date('m/d/Y', $tstamp),
'', false, DisplayLib::standardClearButton());
return $json;
}
}
// start date
if (isset($upc[$pos]) && $upc[$pos] == "4") {
$pos += 1;
$starts = substr($upc,$pos,6);
$pos += 6;
$year = "20".substr($starts,0,2);
$month = substr($starts,2,2);
$dday = substr($starts,4,2);
$tstamp = mktime(0,0,0,$month,$day,$year);
if ($tstamp > time()) {
$json['output'] = DisplayLib::boxMsg(
sprintf(_("Coupon not valid until %d/%d/%d"), $m, $d, $y),
'', false, DisplayLib::standardClearButton());
return $json;
}
}
// serial number
$coupon->serial = false;
if (isset($upc[$pos]) && $upc[$pos] == "5") {
$pos += 1;
$serialLength = ((int)$upc[$pos]) + 6;
$pos += 1;
$coupon->serial = substr($upc,$pos,$serialLength);
$pos += $serialLength;
}
// retailer
$coupon->retailer = false;
if (isset($upc[$pos]) && $upc[$pos] == "6") {
$pos += 1;
$rtLength = ((int)$upc[$pos]) + 6;
$pos += 1;
$coupon->retailer = substr($upc,$pos,$rtLength);
$pos += $rtLength;
}
/* END OPTIONAL FIELDS */
/* STEP 3 - The Miscellaneous Field
This field is also optional, but filling in
the default values here will make code
that validates coupons and calculates values
consistent
*/
$coupon->valueCode = 0;
$coupon->valueApplies = 0;
$coupon->storeCoupon = 0;
$coupon->noMultiply = 0;
if (isset($upc[$pos]) && $upc[$pos] == "9") {
$pos += 1;
$coupon->valueCode = $upc[$pos];
$pos += 1;
$coupon->valueApplies = $upc[$pos];
$pos += 1;
$coupon->storeCoupon = $upc[$pos];
$pos += 1;
$coupon->noMultiply = $upc[$pos];
$pos += 1;
}
/* END Miscellaneous Field */
/* STEP 4 - validate coupon requirements */
list($coupon->firstReq, $json) = $this->validateRequirement($coupon->firstReq, $json);
if (!$coupon->firstReq->valid && (!property_exists($coupon->secondReq, 'value') || $coupon->requiredRulesCode == 1 || $coupon->requiredRulesCode == 2)) {
// if the primary requirement isn't valid and
// a) there are no more requirments, or
// b) the primary requirement is mandatory
// return the json. Error message should have been
// set up by validateRequirement()
return $json;
}
list($coupon->secondReq, $json) = $this->validateRequirement($coupon->secondReq, $json);
if (!$coupon->secondReq->valid && !$coupon->firstReq->valid && (!property_exists($coupon->thirdReq, 'value') || $coupon->requiredRulesCode == 1)) {
// if the secondary requirment isn't valid and
// a) there are no more requirments, or
// b) all requirements are mandatory
// return the json. Error message should have been
// set up by validateRequirement()
return $json;
}
list($coupon->thirdReq, $json) = $this->validateRequirement($coupon->thirdReq, $json);
// compare requirement results with rules
// return error message if applicable
switch ($coupon->requiredRulesCode) {
case '0': // any requirement can be used
if (!$coupon->firstReq->valid && !$coupon->secondReq->valid && !$coupon->thirdReq->valid) {
return $json;
}
break;
case '1': // all required
if (!$coupon->firstReq->valid || !$coupon->secondReq->valid || !$coupon->thirdReq->valid) {
return $json;
}
break;
case '2': // primary + second OR third
if (!$coupon->firstReq->valid) {
return $json;
} elseif (!$coupon->secondReq->valid && !$coupon->thirdReq->valid) {
return $json;
}
break;
case '3': // either second or third. seems odd, may
// be misreading documentation on this one
if (!$coupon->secondReq->valid && !$coupon->thirdReq->valid) {
return $json;
}
break;
default:
$json['output'] = DisplayLib::boxMsg(
_("Malformed coupon"),
'', false, DisplayLib::standardClearButton());
return $json;
}
/* End requirement validation */
/* STEP 5 - determine coupon value */
$valReq = $coupon->firstReq;
if ($coupon->valueApplies == 1) {
$valReq = $coupon->secondReq;
} elseif ($coupon->valueApplies == 2) {
$valReq = $coupon->thirdReq;
}
$value = 0;
switch($coupon->valueCode) {
case '0': // value in cents
case '6':
$value = MiscLib::truncate2($coupon->redeemValue / 100.00);
break;
case '1': // free item
$value = $valReq->price;
break;
case '2': // multiple free items
$value = MiscLib::truncate2($valReq->price * $valReq->value);
break;
case '5': // percent off
$value = MiscLib::truncate2($valReq->price * ($valReq->value/100.00));
break;
default:
$json['output'] = DisplayLib::boxMsg(
_("Error: bad coupon " . $coupon->valueCode),
'', false, DisplayLib::standardClearButton());
return $json;
}
/* attempt to cram company prefix and offer code
into 13 characters
First character is zero
Next characters are company prefix
Remaining characters are offer code
The first zero is there so that the company
prefix will "line up" with matching items in
localtemptrans
Offer code won't always fit. This is just best
effort. I've already seen a real coupon using
a 10 digit prefix. In theory there could even
be a 12 digit prefix leaving no room for the
offer code at all.
*/
$upcStart = "0" . $valReq->prefix;
$remaining = 13 - strlen($upcStart);
if (strlen($offer) < $remaining) {
$offer = str_pad($offer,$remaining,'0',STR_PAD_LEFT);
} elseif (strlen($offer) > $remaining) {
$offer = substr($offer,0,$remaining);
}
$couponUPC = $upcStart.$offer;
TransRecord::addCoupon($couponUPC, $coupon->firstReq->department, -1*$value);
$json['output'] = DisplayLib::lastpage();
return $json;
}
/* method takes an requirement array and a json array,
both by reference
item price is added to the requirements array as it
can be needed to calculate coupon value later
json array is updated to include error messages if
the requirement isn't met, but these are not necessarily
fatal errors when there are multiple requirements
return true/false based on whether requirement is met
*/
private function validateRequirement($req, $json)
{
// non-existant requirement is treated as valid
if (!property_exists($req, 'value')) {
$req->valid = true;
return array($req, $json);
}
$dbc = Database::tDataConnect();
/* simple case first; just wants total transaction value
* no company prefixing of items.
* First check for "already applied".
*/
if ($req->code == 2) {
/* Compose the coupon upc value from the prefix and the offer code.
*/
$upcStart = "0" . $req->prefix;
$offer = $this->offerCode;
$remaining = 13 - strlen($upcStart);
if (strlen($offer) < $remaining) {
$offer = str_pad($offer,$remaining,'0',STR_PAD_LEFT);
} elseif (strlen($offer) > $remaining) {
$offer = substr($offer,0,$remaining);
}
$couponUPC = $upcStart.$offer;
// See if the coupon has already been applied.
$dupQ = "SELECT sum(CASE WHEN trans_status='C' THEN 1 ELSE 0 END) as couponqtty
FROM localtemptrans
WHERE upc = ?";
$dupS = $dbc->prepare($dupQ);
$dupR = $dbc->execute($dupS, array($couponUPC));
if ($dbc->numRows($dupR) > 0) {
$dupRow = $dbc->fetchRow($dupR);
if ($dupRow['couponqtty'] != null && $dupRow['couponqtty'] > 0) {
$json['output'] = DisplayLib::boxMsg(
_("Coupon already applied"),
'', false, DisplayLib::standardClearButton());
$req->valid = false;
return array($req, $json);
}
}
$req->department = 0;
return $this->validateTransactionTotal($req, $json);
}
$req->valid = false;
/* Totals and values from coupon and non-coupon items
* with upc's that match the company prefix (brand).
*/
$query = sprintf("SELECT
max(CASE WHEN trans_status<>'C' THEN unitPrice ELSE 0 END) as price,
sum(CASE WHEN trans_status<>'C' THEN total ELSE 0 END) as total,
max(department) as department,
sum(CASE WHEN trans_status<>'C' THEN ItemQtty ELSE 0 END) as qty,
sum(CASE WHEN trans_status='C' THEN 1 ELSE 0 END) as couponqtty
FROM localtemptrans WHERE
substring(upc,2,%d) = '%s'",
strlen($req->prefix),$req->prefix);
$result = $dbc->query($query);
/* If there are no prefix matches it returns a row of NULLs
* not an empty set.
*/
if ($dbc->numRows($result) <= 0) {
$json['output'] = DisplayLib::boxMsg(
_("Coupon requirements not met"),
'', false, DisplayLib::standardClearButton());
return array($req, $json);
}
$row = $dbc->fetchRow($result);
if ($row['price'] == null && $row['total'] == null &&
$row['department'] == null && $row['qty'] == null &&
$row['couponqtty'] == null) {
$json['output'] = DisplayLib::boxMsg(
_("No items from the issuer of this coupon"),
'', false, DisplayLib::standardClearButton());
return array($req, $json);
}
$req->price = $row['price'];
$req->department = $row['department'];
switch($req->code) {
case '0': // various qtty requirements
case '3':
case '4':
return $this->validateQty($row['qty'], $row['couponqtty'], $req, $json);
case '1':
return $this->validateQty($row['total'], $row['couponqtty'], $req, $json);
case '9':
$json['output'] = DisplayLib::boxMsg(
_("Tender coupon manually"),
'', false, DisplayLib::standardClearButton());
return array($req, $json);
default:
$json['output'] = DisplayLib::boxMsg(
_("Error: bad coupon"),
'', false, DisplayLib::standardClearButton());
return array($req, $json);
}
$req->valid = true;
return array($req, $json); // requirement validated
}
private function validateTransactionTotal($req, $json)
{
$dbc = Database::tDataConnect();
$req->valid = false;
$chkQ = "SELECT SUM(total) FROM localtemptrans WHERE
trans_type IN ('I','D','M')";
$chkR = $dbc->query($chkQ);
$ttlRequired = MiscLib::truncate2($req->value / 100.00);
if ($dbc->num_rows($chkR) == 0) {
$json['output'] = DisplayLib::boxMsg(
sprintf(_("Coupon requires transaction of at least \$%.2f"), $ttlRequired),
'', false, DisplayLib::standardClearButton());
return array($req, $json);
}
$chkW = $dbc->fetch_row($chkR);
if ($chkW[0] < $ttlRequired) {
$json['output'] = DisplayLib::boxMsg(
sprintf(_("Coupon requires transaction of at least \$%.2f"), $ttlRequired),
'', false, DisplayLib::standardClearButton());
return array($req, $json);
}
$req->valid = true;
return array($req, $json);
}
private function validateQty($qty, $couponqtty, $req, $json)
{
$available_qty = $qty - ($couponqtty * $req->value);
if ($available_qty < $req->value) {
// Coupon requirement not met
if ($couponqtty > 0) {
$json['output'] = DisplayLib::boxMsg(
_("Coupon already applied"),
'', false, DisplayLib::standardClearButton());
} else {
$json['output'] = DisplayLib::boxMsg(
sprintf(_("Coupon requires %d items"), $req->value),
'', false, DisplayLib::standardClearButton());
}
$req->valid = false;
return array($req, $json);
}
$req->valid = true;
return array($req, $json);
}
}
/*
$obj = new DatabarCoupon();
$obj->handle("8110100707340143853100110110",array());
$obj->handle("811010041570000752310011020096000",array());
$obj->handle("8110007487303085831001200003101130",array());
$obj->handle("811050860006354409292341000410002000324022996000",array()); // NCG, code 2, $10
*/