From 779fdbdef110c95220806a2a33bd5554086bf004 Mon Sep 17 00:00:00 2001 From: Konrad Rosenbaum Date: Sun, 1 Jan 2017 21:37:35 +0100 Subject: [PATCH] implement validation of seat plans and coupons Change-Id: Id0a87c2d83e8c4891ab453b1c1c21f71fca7d59d --- commonlib/mapplication.cpp | 20 ++- doc/seatplans.txt | 5 +- pack | 2 +- sessman/login.cpp | 4 +- src/dialogs/eventedit.cpp | 102 +++++++++++++++ src/dialogs/eventedit.h | 9 +- src/dialogs/orderwin.cpp | 4 + src/dialogs/orderwin.h | 2 +- src/mwin/carttab.cpp | 65 +++++++++- src/mwin/carttab.h | 4 + taurus | 2 +- wob/classes/cart.wolf | 28 ++++- wob/classes/event.wolf | 81 ++++++++++++- wob/classes/seatplan.wolf | 78 +----------- wob/db/cart.wolf | 4 + wob/db/db.wolf | 2 +- wob/magicsmoke.wolf | 6 +- wob/transact/event.wolf | 27 ++++- www/inc/classes/autoload.php | 4 +- www/inc/classes/eventplan.php | 256 +++++++++++++++++++++++++++++++++++++ www/inc/wext/autoload.php | 6 +- www/inc/wext/cart.php | 71 +++-------- www/inc/wext/event.php | 2 + www/inc/wext/seatplan.php | 281 +++++++++++++++++++++++++++++++++++++++++ www/inc/wext/ticket.php | 8 +- 25 files changed, 908 insertions(+), 165 deletions(-) create mode 100644 www/inc/classes/eventplan.php create mode 100644 www/inc/wext/seatplan.php diff --git a/commonlib/mapplication.cpp b/commonlib/mapplication.cpp index e9f8aa1..176f215 100644 --- a/commonlib/mapplication.cpp +++ b/commonlib/mapplication.cpp @@ -118,6 +118,9 @@ class MProgressWrapperGui:public MProgressWrapper #define HOMEPAGE_BASEURL "http://smoke.silmor.de" #endif +#define GIT_DATE_FORMAT "yyyy-MM-dd HH:mm:ss" +#define BUILD_DATE_FORMAT Qt::ISODate + void MApplication::aboutMS() { QMessageBox mb; @@ -125,17 +128,20 @@ void MApplication::aboutMS() mb.setWindowTitle(tr("About MagicSmoke")); MSInterface*ifc=MSInterface::instance(); mb.setText(tr( "

MagicSmoke v. %1

" - "© Konrad Rosenbaum, 2007-2013
" + "© Konrad Rosenbaum, 2007-%6
" //I'm lazy, but assuming I'm still the principal author... "© Peter Keller, 2007-2008
" "protected under the GNU GPL v.3 or at your option any newer

" "See also the MagicSmoke Homepage.

" - "This version was compiled from repository '%3' revision '%4'.

" + "This version was compiled at %8
from repository '%3'
revision '%4'
last changed %7.

" "The current application data base path is '%5'.") - .arg(MSInterface::staticVersionInfo(WOb::VersionHR)) //%1 - .arg(HOMEPAGE_BASEURL) //%2 - .arg(MSInterface::staticVersionInfo(WOb::VersionRootURL)) //%3 - .arg(MSInterface::staticVersionInfo(WOb::VersionNumber)) //%4 - .arg(ifc?ifc->dataDir(): dataDir()) + .arg(MSInterface::staticVersionInfo(WOb::VersionHR)) //%1 - human readable version + .arg(HOMEPAGE_BASEURL) //%2 - Homepage + .arg(MSInterface::staticVersionInfo(WOb::VersionRootURL)) //%3 - main (remotes/origin) GIT repo + .arg(MSInterface::staticVersionInfo(WOb::VersionNumber)) //%4 - rev ID + .arg(ifc?ifc->dataDir(): dataDir())//%5 - $HOME/.magicSmoke2 + .arg(MSInterface::staticVersionInfo(WOb::VersionTime).left(4))//%6 - year of the last modification + .arg(MSInterface::staticVersionInfo(WOb::VersionTime)) //%7 - last modification in GIT + .arg(MSInterface::staticVersionInfo(WOb::VersionGenTime)) //%8 - compile time (of iface.so) ); mb.setStandardButtons(QMessageBox::Ok); mb.exec(); diff --git a/doc/seatplans.txt b/doc/seatplans.txt index 210c429..13c7ac1 100644 --- a/doc/seatplans.txt +++ b/doc/seatplans.txt @@ -6,7 +6,7 @@ Specification Version (YYYYnn): 201601 Server Side Minimum: --------------------- - + @@ -20,6 +20,9 @@ In both cases seats are unnumbered (i.e. audience members can chose freely). defines the complete plan version - version number of this spec, it is generally assumed that newer versions are backwards compatible + exclusive - bool whether only categories listed in the plan can be sold + true: unlisted categories are blocked from sale, + false: unlisted categories are unrestricted by the plan defines one physical group of seats capacity - specifies how many seats there are in this group id - automatically generated ID for groups, must only contain letters and diff --git a/pack b/pack index c214783..f2a5d37 160000 --- a/pack +++ b/pack @@ -1 +1 @@ -Subproject commit c214783152ecdbf1cf2b9715ab3328ec81716b2e +Subproject commit f2a5d37c76272477abc5f5a1c668b7d1467be0ef diff --git a/sessman/login.cpp b/sessman/login.cpp index 958107a..6f3feab 100644 --- a/sessman/login.cpp +++ b/sessman/login.cpp @@ -34,6 +34,8 @@ #include #include +#include "mapplication.h" + MLogin::MLogin(MSessionManager*sm) { setWindowTitle(tr("Magic Smoke Login")); @@ -48,7 +50,7 @@ MLogin::MLogin(MSessionManager*sm) m=mb->addMenu(tr("&Configure")); m->addAction(tr("&Configuration..."),this,SLOT(configwin())); m->addAction(tr("Client &Selection..."),this,SLOT(clientConfig())); -// mb->addMenu(MApplication::helpMenu()); + mb->addMenu(MApplication::helpMenu()); //create central widget QGridLayout*gl; diff --git a/src/dialogs/eventedit.cpp b/src/dialogs/eventedit.cpp index b09e85c..f26f312 100644 --- a/src/dialogs/eventedit.cpp +++ b/src/dialogs/eventedit.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -39,6 +41,9 @@ #include "MTCreateRoom" #include "MTGetAllArtists" #include "MTCreateArtist" +#include "MTGetAllSeatPlans" +#include "MTCreateSeatPlan" +#include "MTUpdateSeatPlan" #include "centbox.h" #include "pricecatdlg.h" @@ -145,6 +150,15 @@ MEventEditor::MEventEditor(QWidget*pw, OpenMode mode, qint64 id) hl->addWidget(p=new QPushButton("..."),0); connect(p,SIGNAL(clicked()),this,SLOT(selectRoom())); + gl->addWidget(lab=new QLabel(tr("Seat Plan:")),++lctr,0); + lab->setAlignment(Qt::AlignRight|Qt::AlignVCenter); + gl->addLayout(hl=new QHBoxLayout,lctr,1); + hl->addWidget(seatplan=new QLineEdit,10); + seatplan->setReadOnly(true); + seatplan->setText(event.seatplan().data().name()); + hl->addWidget(p=new QPushButton("..."),0); + connect(p,SIGNAL(clicked()),this,SLOT(selectSeatPlan())); + gl->addWidget(lab=new QLabel(tr("Capacity:")),++lctr,0); lab->setAlignment(Qt::AlignRight|Qt::AlignVCenter); gl->addWidget(capacity=new QSpinBox,lctr,1); @@ -218,6 +232,10 @@ void MEventEditor::writeBack() event.setcapacity(capacity->value()); event.setroom(room->text()); event.setflags(flags->text()); + if(seatplanid>=0) + event.setseatplanid(seatplanid); + else + event.setseatplanid(NullValue()); //artist is set in other methods already //prices are changed by the methods reacting to the table //send to server @@ -413,6 +431,58 @@ void MEventEditor::selectRoom() room->setText(wlst[0]->text()); } +void MEventEditor::selectSeatPlan() +{ + QListrlst=req->queryGetAllSeatPlans().getplans(); + QDialog d(this); + d.setWindowTitle(tr("Select a Seat Plan")); + QVBoxLayout*vl; + d.setLayout(vl=new QVBoxLayout); + QListView*rlstw; + QStandardItemModel model; + vl->addWidget(rlstw=new QListView,10); + rlstw->setModel(&model); + model.insertColumns(0,1); + model.insertRows(0,rlst.size()); + qDebug()<<"plans:"<addLayout(hl=new QHBoxLayout,0); + hl->addStretch(10); + QPushButton*p; + hl->addWidget(p=new QPushButton(tr("New...","new seatplan")),0); + connect(p,SIGNAL(clicked()),this,SLOT(newSeatPlan())); + connect(p,SIGNAL(clicked()),&d,SLOT(reject())); + p->setEnabled(req->hasRight(req->RCreateSeatPlan)); + hl->addWidget(p=new QPushButton(tr("Select","select seatplan")),0); + connect(p,SIGNAL(clicked()),&d,SLOT(accept())); + hl->addWidget(p=new QPushButton(tr("Remove Seat Plan","remove seatplan from event")),0); + connect(p,&QPushButton::clicked,&d,[&](){ + seatplanid=-1; + seatplan->setText(QString()); + d.reject(); + }); + hl->addWidget(p=new QPushButton(tr("Cancel")),0); + connect(p,SIGNAL(clicked()),&d,SLOT(reject())); + if(d.exec()==QDialog::Rejected)return; + //get selection + const QModelIndex cidx=rlstw->currentIndex(); + if(!cidx.isValid())return; + bool b=false; + seatplanid=model.data(cidx,Qt::UserRole).toInt(&b); + if(b) + seatplan->setText(model.data(cidx).toString()); + else{ + seatplan->setText(QString()); + seatplanid=-1; + } +} + void MEventEditor::newRoom() { //TODO: do more intelligent input for new room @@ -427,6 +497,38 @@ void MEventEditor::newRoom() } } +void MEventEditor::newSeatPlan() +{ + if(room->text().isEmpty()){ + QMessageBox::warning(this,tr("Warning"),tr("Please select a room first, since a seat plan must have a room.")); + return; + } + QString fname=QFileDialog::getOpenFileName(this,tr("Please select a seat plan file")); + if(fname.isEmpty())return; + QString plan; + QFile fd(fname); + if(!fd.open(QIODevice::ReadOnly)){ + QMessageBox::warning(this,tr("Warning"),tr("Cannot open seat plan file '%1'.").arg(fname)); + return; + } + //TODO: fix detection of encoding + plan=QString::fromUtf8(fd.readAll()); + fd.close(); + QString name=QInputDialog::getText(this,tr("Seat Plan Name"),tr("Please enter a name for your seat plan:")); + if(name.isEmpty())return; + MOSeatPlanInfo sp; + sp.setname(name); + sp.setroomid(room->text()); + sp.setplan(plan); + MTCreateSeatPlan csp=req->queryCreateSeatPlan(sp); + if(csp.hasError()){ + QMessageBox::warning(this,tr("Warning"),tr("Error while creating seat plan: %1").arg(csp.errorString())); + return; + } + seatplanid=csp.getplan().value().seatplanid(); + seatplan->setText(csp.getplan().value().name()); +} + void MEventEditor::selectArtist() { QListrlst=req->queryGetAllArtists().getartists(); diff --git a/src/dialogs/eventedit.h b/src/dialogs/eventedit.h index 161fcad..ffa8a67 100644 --- a/src/dialogs/eventedit.h +++ b/src/dialogs/eventedit.h @@ -54,7 +54,11 @@ class MEventEditor:public QDialog void selectRoom(); /**create a new room*/ void newRoom(); - + /**seatplan button has been clicked: open a list of plans, select one*/ + void selectSeatPlan(); + ///create a new seat plan + void newSeatPlan(); + /**artist button has been clicked: open a list of artists, select one*/ void selectArtist(); /**create a new artist*/ @@ -77,7 +81,7 @@ class MEventEditor:public QDialog private: MOEvent event; QDateTimeEdit*starttime,*endtime; - QLineEdit*title,*artist,*room,*cancelreason; + QLineEdit*title,*artist,*room,*cancelreason,*seatplan; QTextEdit*description,*comment; QCheckBox*cancelcheck; QSpinBox*capacity; @@ -85,6 +89,7 @@ class MEventEditor:public QDialog MFlagWidget*flags; QTableView*pricetable; QStandardItemModel*pricemodel; + int seatplanid=-1; }; class MCentSpinBox; diff --git a/src/dialogs/orderwin.cpp b/src/dialogs/orderwin.cpp index 40bb9f5..4d7ee78 100644 --- a/src/dialogs/orderwin.cpp +++ b/src/dialogs/orderwin.cpp @@ -194,6 +194,8 @@ MOrderWindow::MOrderWindow(QWidget*par,const MOOrder&o) gl->addWidget(m_total=new QLabel(),rw,1); gl->addWidget(new QLabel(tr("Already Paid:")),++rw,0); gl->addWidget(m_paid=new QLabel(),rw,1); + gl->addWidget(new QLabel(tr("Coupon:")),++rw,0); + gl->addWidget(m_coupon=new QLabel(),rw,1); gl->addWidget(new QLabel(tr("Sold by:")),++rw,0); gl->addWidget(m_soldby=new QLabel(),rw,1); gl->addWidget(lab=new QLabel(tr("Order Comments:")),++rw,0); @@ -240,6 +242,8 @@ void MOrderWindow::updateData() m_paid->setText(m_order.amountPaidString()); m_soldby->setText(m_order.soldby()); m_comment->setText(m_order.comments()); + if(m_order.couponid().isNull())m_coupon->setText(tr("(none)","no coupon")); + else m_coupon->setText(QString("%1 (%2)").arg(m_order.couponid().data()).arg(m_order.coupondescription().data())); //get detail data QList tickets=m_order.tickets(); QList events; diff --git a/src/dialogs/orderwin.h b/src/dialogs/orderwin.h index 757f05f..792e5b9 100644 --- a/src/dialogs/orderwin.h +++ b/src/dialogs/orderwin.h @@ -111,7 +111,7 @@ class MOrderWindow:public QMainWindow MOOrder m_order; bool m_changed; QLabel *m_orderid,*m_orderdate,*m_sentdate,*m_state,*m_paid,*m_total,*m_comment, - *m_shipmeth,*m_shipprice,*m_daddr,*m_iaddr,*m_soldby,*m_custname; + *m_shipmeth,*m_shipprice,*m_daddr,*m_iaddr,*m_soldby,*m_custname,*m_coupon; QTableView *m_table; QStandardItemModel *m_model; QAction*m_res2order,*m_cancel,*m_ship,*m_pay,*m_payv,*m_refund; diff --git a/src/mwin/carttab.cpp b/src/mwin/carttab.cpp index 4b29258..e1bda4e 100644 --- a/src/mwin/carttab.cpp +++ b/src/mwin/carttab.cpp @@ -382,6 +382,8 @@ void MCartTab::addTicketForEvent(qint64 id, qint64 prcid) QString pcn=cent2str(ep[pcidx].price())+" ("+ep[pcidx].pricecategory().value().name().value()+")"; cartmodel->setData(cartmodel->index(cr,3),pcn); carttable->resizeColumnsToContents(); + //adjust coupon calcs + applyCoupon(); } void MCartTab::eventOrderTicket() @@ -508,6 +510,9 @@ void MCartTab::cartOrder(bool isreserve,bool issale) //is there a comment? QString s=cartcomment->toPlainText().trimmed(); if(s!="")cord.setcomment(s); + //coupon? + if(!couponinfo.couponid().isNull()) + cord.setcouponid(couponinfo.couponid()); //scan tickets & scan vouchers cartTableToOrder(cord); //set shipping info @@ -568,6 +573,10 @@ bool MCartTab::canorder(bool isreserve) QMessageBox::warning(this,tr("Warning"),tr("Reservations can only contain tickets.")); return false; } + if(vouchersforpay.size()>0){ + QMessageBox::warning(this,tr("Warning"),tr("Reservations cannot contain any vouchers for payment yet.")); + return false; + } } //all clear return true; @@ -583,6 +592,7 @@ void MCartTab::cartTableToOrder(MOCartOrder&cord) ct.setamount(cartmodel->data(idx).toInt()); ct.seteventid(cartmodel->data(idx,CART_IDROLE).toInt()); ct.setpricecategoryid(cartmodel->data(idx,CART_PRICEIDROLE).toInt()); + ct.setorigpricecategoryid(cartmodel->data(idx,CART_ORIGPRICEIDROLE).toInt()); ct.setcartlineid(i); cord.addtickets(ct); }else @@ -600,7 +610,6 @@ void MCartTab::cartTableToOrder(MOCartOrder&cord) bool MCartTab::orderExecute(MOCartOrder&cord,MOOrder&ord,bool isreserve,bool issale) { - //TODO: add coupon and related info? if(isreserve){ MTCreateReservation co=req->queryCreateReservation(cord); if(co.hasError()){ @@ -610,17 +619,54 @@ bool MCartTab::orderExecute(MOCartOrder&cord,MOOrder&ord,bool isreserve,bool iss ord=co.getorder(); cord=co.getcart(); }else{ - MTCreateOrder co=req->queryCreateOrder(cord,issale,QStringList());//TODO: add vouchers + QStringList vouchers; + for(auto v:vouchersforpay)vouchers<queryCreateOrder(cord,issale,vouchers); if(co.hasError()){ QMessageBox::warning(this,tr("Warning"),tr("Error while creating order: %1").arg(co.errorString())); return false; } ord=co.getorder(); cord=co.getcart(); + if(vouchers.size()>0) + displayUsedVouchers(co.getvouchers(),co.getpaidcash()); } return true; } +void MCartTab::displayUsedVouchers(const QList&vouchers,int paidcash) +{ + QDialog d(this); + d.setWindowTitle(tr("Voucher State")); + d.setSizeGripEnabled(true); + QVBoxLayout *vl; + d.setLayout(vl=new QVBoxLayout); + QTableView *tv; + QStandardItemModel *tm; + vl->addWidget(tv=new QTableView,1); + tv->setModel(tm=new QStandardItemModel(&d)); + tv->setEditTriggers(QAbstractItemView::NoEditTriggers); + tm->setHorizontalHeaderLabels(QStringList()<insertRows(vouchers.size(),0); + for(int i=0;isetData(tm->index(i,0),vouchers[i].voucherid().data()); + int oprc=0; + for(auto v:vouchersforpay)if(v.first==vouchers[i].voucherid())oprc=v.second; + tm->setData(tm->index(i,1),cent2str(oprc)); + tm->setData(tm->index(i,2),cent2str(oprc-vouchers[i].value())); + tm->setData(tm->index(i,3),cent2str(vouchers[i].value())); + } + vl->addWidget(new QLabel(tr("Paid in Cash: %1").arg(cent2str(paidcash)))); + vl->addSpacing(15); + QHBoxLayout*hl; + vl->addLayout(hl=new QHBoxLayout); + hl->addStretch(1); + QPushButton*p; + hl->addWidget(p=new QPushButton(tr("Ok"))); + connect(p,SIGNAL(clicked()),&d,SLOT(accept())); + d.exec(); +} + void MCartTab::verifyOrderCustomer(MOCartOrder&cord) { QPalette red=QLabel().palette();red.setColor(QPalette::Window,Qt::red); @@ -688,13 +734,13 @@ void MCartTab::verifyOrderTickets(const QList&ticks) break; case MOCartTicket::Exhausted: cartmodel->setData(idx0,red,Qt::BackgroundRole); - tt=tr("The event or category is (almost) sold out, there are %1 tickets left.").arg(ticks[i].maxamount()); + tt=tr("The event or category is (almost) sold out, there are %1 tickets left. (%2)").arg(ticks[i].maxamount()).arg(ticks[i].statustext()); cartmodel->setData(idx0,tt,Qt::ToolTipRole); cartmodel->setData(idx1,tt,Qt::ToolTipRole); cartmodel->setData(idxn,tt); break; case MOCartTicket::Invalid: - tt=tr("The event does not exist or there is another serious problem, please remove this entry."); + tt=tr("The event does not exist or there is another serious problem, please remove this entry. (%1)").arg(ticks[i].statustext()); cartmodel->setData(idx1,tt,Qt::ToolTipRole); cartmodel->setData(idxn,tt); break; @@ -826,6 +872,8 @@ void MCartTab::changeVoucher ( QString url ) couponinfo=cp; applyCoupon(); }else if(url=="vadd:voucher"){ + QMessageBox::warning(this,tr("Warning"),tr("Sorry, not implemented yet. Please wait a few days.")); + return;/* //sanity check bool priv=req->hasRight(MInterface::PCreateOrder_CanPayVoucherWithVoucher); bool vfound=false; @@ -861,7 +909,7 @@ void MCartTab::changeVoucher ( QString url ) return; } //add - vouchersforpay.append(QPair(vid,vou.getvoucher().data().value())); + vouchersforpay.append(QPair(vid,vou.getvoucher().data().value()));*/ }else if(url.startsWith("vremove:")){ const QString vid=url.mid(8); for(int i=0;iindex(i,0); if(cartmodel->data(idx0,CART_TYPEROLE).toInt()!=CART_TICKET)continue; MOEvent ev=cartmodel->data(idx0,CART_EVENTDATAROLE).value(); - //QModelIndex idxp=cartmodel->index(i,COL_PRICE); int pid=cartmodel->data(idx0,CART_ORIGPRICEIDROLE).toInt(); MOEventPrice ep; for(auto prc:ev.price()) @@ -920,7 +967,7 @@ void MCartTab::applyCoupon() } //go through rules and adjust tickets - //TODO: work on accumulated data + //TODO: work on accumulated data and observe limits for(int i=0;irowCount();i++){ //get price id QModelIndex idx0=cartmodel->index(i,0); @@ -944,7 +991,10 @@ void MCartTab::applyCoupon() if(prc.pricecategoryid()==pid) epo=prc; } + //ignore if not exist + if(ep.eventid().isNull() || epo.eventid().isNull())continue; + //change table cartmodel->setData(idx0,npid,CART_PRICEIDROLE); cartmodel->setData(idx0,ep.price().data(),CART_PRICEROLE); QModelIndex idxp=cartmodel->index(i,COL_PRICE); @@ -952,6 +1002,7 @@ void MCartTab::applyCoupon() QModelIndex idxn=cartmodel->index(i,COL_NOTES); cartmodel->setData(idxn,tr("Original price: %1 (%2)").arg(cent2str(epo.price())).arg(epo.pricecategory().value().name().value())); } + carttable->resizeColumnsToContents(); } /********************************************************************************/ diff --git a/src/mwin/carttab.h b/src/mwin/carttab.h index 6f3ddb9..d391680 100644 --- a/src/mwin/carttab.h +++ b/src/mwin/carttab.h @@ -41,6 +41,7 @@ class MOCartItem; class MOCartTicket; class MOCartVoucher; class MOOrder; +class MOVoucher; /**Main Overview Window: cart tab*/ class MCartTab:public QWidget @@ -144,6 +145,9 @@ class MCartTab:public QWidget ///helper to apply coupon rules void applyCoupon(); + + ///helper to display vouchers after a sale/order + void displayUsedVouchers(const QList&,int paidcash); }; /**Helper class for shopping cart: allow editing amount, but nothing else*/ diff --git a/taurus b/taurus index a726652..6367bd9 160000 --- a/taurus +++ b/taurus @@ -1 +1 @@ -Subproject commit a72665280c834c63349a6d0cfd4f6d09355b86fd +Subproject commit 6367bd9faad45fb7c346875c8fdb25077121d50f diff --git a/wob/classes/cart.wolf b/wob/classes/cart.wolf index cfc7e99..0debe14 100644 --- a/wob/classes/cart.wolf +++ b/wob/classes/cart.wolf @@ -20,13 +20,16 @@ optional property that can be used by the calling process to distinguish lines in the cart, the server must preserve it unchanged - + Price category to be used for this ticket. + Price category originally assigned before the coupon changed it. + The cartID as used by the web user interface, this property must not be interpreted while the server attempts to create an order from this cart, the server must preserve it unchanged Used in the Web UI only to provide access to price properties + Used in the Web UI only to provide access to price properties Used in the Web UI only to provide access to event properties @@ -34,11 +37,15 @@ + + + + @@ -94,14 +101,16 @@ - An optional coupon code to be used in this order. + An optional coupon code to be used in this order. + + @@ -115,9 +124,11 @@ - + inc/wext/webcart.php - WOWebCart - is the backend part, that cares about conversion to/from the database, it contains all convenience objects as well + inc/wext/cart.php - WOCartOrder - used for the placeOrder transaction only and contains the bare minimum of data + --> The cart as used by the web user interface, this maps into the cart tables. This class is never used by the remote client. The cart ID of this session @@ -138,7 +149,10 @@ shop items inside this cart as seen in the DB if shipping is set: the shipping type - + + An optional coupon code to be used in this order. + + @@ -147,6 +161,7 @@ + @@ -168,6 +183,9 @@ + + + diff --git a/wob/classes/event.wolf b/wob/classes/event.wolf index b90414a..86b8325 100644 --- a/wob/classes/event.wolf +++ b/wob/classes/event.wolf @@ -2,11 +2,32 @@ + + + + This class is the transport for seat plans when communicating over the wire + + + + + + + + + + + + + + + + + @@ -116,6 +137,7 @@ + @@ -162,6 +184,63 @@ + + + + + + + + + This class represents only the info from a ticket that is required for calculating seating information. + + + + + + + + + + + + + + + + + + + + This class transports all data necessary to calculate the free seats of an event + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wob/classes/seatplan.wolf b/wob/classes/seatplan.wolf index a582776..f59fbd9 100644 --- a/wob/classes/seatplan.wolf +++ b/wob/classes/seatplan.wolf @@ -7,7 +7,7 @@ - see COPYING.AGPL for details --> - + @@ -73,85 +73,15 @@ + This class is used to parse seat plans on either side. It is not sent over the wire, since it is just a helper class. - + Version of the SeatPlan spec that was implemented. + If true: only categories listed can be sold, if false: unlisted categories are not restricted - - - - - This class is the transport for seat plans when communicating over the wire - - - - - - - - - - - - - - - - This class represents only the info from a ticket that is required for calculating seating information. - - - - - - - - - - - - - - - - - - - - This class transports all data necessary to calculate the free seats of an event - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/wob/db/cart.wolf b/wob/db/cart.wolf index accfa25..dc36358 100644 --- a/wob/db/cart.wolf +++ b/wob/db/cart.wolf @@ -28,6 +28,9 @@ pointer to shipping type (none per default, programmatic default is in config) + + If not null: the ID of the coupon that will be used for the order. + web interface: stores the tickets in the cart @@ -35,6 +38,7 @@ the event the ticket is forthe price category of the ticket + the price category of the ticketamount of tickets of this type in the cart
diff --git a/wob/db/db.wolf b/wob/db/db.wolf index d4f5705..f3fd3ef 100644 --- a/wob/db/db.wolf +++ b/wob/db/db.wolf @@ -9,7 +9,7 @@ + version="01.10" versionRow="MagicSmokeVersion"> Time at which the change was made. diff --git a/wob/magicsmoke.wolf b/wob/magicsmoke.wolf index cb70db4..201ecdf 100644 --- a/wob/magicsmoke.wolf +++ b/wob/magicsmoke.wolf @@ -2,18 +2,18 @@ These files describe the database schema and communication protocol of MagicSmoke. - &copy; Konrad Rosenbaum, 2009-2015 + &copy; Konrad Rosenbaum, 2009-2017 <br/>these files are protected under the GNU AGPLv3 or at your option any newer - + diff --git a/wob/transact/event.wolf b/wob/transact/event.wolf index 97043ea..b2ac49e 100644 --- a/wob/transact/event.wolf +++ b/wob/transact/event.wolf @@ -151,7 +151,32 @@ - + + + Returns a list of all existing seat plans + + + + + + + + + + + + + + + + + + + + + + + Returns the event plus all orders concerning it diff --git a/www/inc/classes/autoload.php b/www/inc/classes/autoload.php index 85a69d5..4fec57d 100644 --- a/www/inc/classes/autoload.php +++ b/www/inc/classes/autoload.php @@ -1,5 +1,5 @@ diff --git a/www/inc/classes/eventplan.php b/www/inc/classes/eventplan.php new file mode 100644 index 0000000..31d59bc --- /dev/null +++ b/www/inc/classes/eventplan.php @@ -0,0 +1,256 @@ +newtickets=array(); + $this->oldtickets=array(); + $this->numcattickets=array(); + $et=WTevent::getFromDB($eventid); + if(!is_a($et,"WTevent"))return; + $this->event=WOEvent::fromTableevent($et); + $seatplaninfo=$this->event->getseatplan(); + if(is_a($seatplaninfo,"WOSeatPlanInfo")){ + $this->seatplan=WOSeatPlan::fromString($seatplaninfo->getplan()); + } + global $db; + $this->oldtickets=WOTicket::fromTableArrayticket(WTticket::selectFromDB("eventid=".$db->escapeInt($eventid))); + + //initialize plan fields, TODO: use the concept of seat number + if($this->seatplan!==null) + $this->seatplan->initGroups($eventid); + foreach($this->event->getprice() as $prc) + $this->numcattickets[$prc->getpricecategoryid()]=0; + foreach($this->oldtickets as $tick){ + if(!$tick->isBlocking())continue; + $this->numtickets++; + $this->numcattickets[$tick->getpricecategoryid()]++; + if($this->seatplan!==null) + $this->seatplan->addTickets(1,$tick->getpricecategoryid()); + } + } + + ///returns true if the references event exists + public function eventIsValid() + { + return $this->event !== null; + } + + ///returns true if the user can sell for this event in general, ie.: + /// - it exists + /// - the user has the right to do it + /// - does not check whether it is too late to sell + public function canSellEvent() + { + global $session; + if(!$this->eventIsValid())return false; + if(!$session->checkFlags($this->event->getflags()))return false; + return true; + } + + ///checks whether the event has already started at the given timestamp + public function eventHasStarted($timestamp) + { + return $timestamp>=$this->event->getstarttime(); + } + + ///checks whether the event has already ended at the given timestamp + public function eventHasEnded($timestamp) + { + return $timestamp>=$this->event->getendtime(); + } + + ///adds the ticket to the list of new tickets and validates whether the sale is possible; + ///does not validate the sale timing, this is done in the cart using eventHasStarted and eventHasEnded; + ///adjusts the ticket state in case of failure + public function addTickets($key,&$ticket) + { + $this->newtickets[$key]=&$ticket; + $num=$ticket->getamount(); + $catid=$ticket->getpricecategoryid(); + $ocatid=$ticket->getorigpricecategoryid(); + if(!is_numeric($ocatid))$ocatid=$catid; + //basic checks + if($num<=0){ + $ticket->setstatus(WOCartTicket::Invalid); + $ticket->setstatustext(tr("Cannot sell a negative amount of tickets.")); + return false; + } + if(!is_numeric($catid)){ + $ticket->setstatus(WOCartTicket::Invalid); + $ticket->setstatustext(tr("The category is not numeric.")); + return false; + } + if(!$this->canSellEvent()){ + $ticket->setstatus(WOCartTicket::Invalid); + $ticket->setstatustext(tr("Not a valid event or you do not have access to it.")); + return false; + } + //seats available in event + $avail=$this->event->getcapacity()-$this->numtickets; + if($num>$avail){ + $ticket->setstatus(WOCartTicket::Exhausted); + $ticket->setstatustext(tr("Not enough tickets in this event.")); + $ticket->setmaxamount($avail); + return false; + } + //event has both categories, set price + $havcat=false;$havocat=false;$maxcat=0;$catobj=null; + foreach($this->event->getprice() as $prc){ + if($catid==$prc->getpricecategoryid()){ + $havcat=true; + $ticket->setprice($prc->getprice()); + $maxcat=$prc->getmaxavailable(); + } + if($ocatid==$prc->getpricecategoryid()){ + $havocat=true; + $catobj=$prc; + } + } + if(!$havcat || !$havocat){ + $ticket->setstatus(WOCartTicket::Invalid); + $ticket->setstatustext(tr("The category is not part of the event.")); + return false; + } + //have access to orig category (new cat is checked later in coupon) + global $session; + if(!$session->checkFlags($catobj->getflags())){ + $ticket->setstatus(WOCartTicket::Invalid); + $ticket->setstatustext(tr("You cannot order in this category.")); + return false; + } + //category has enough seats + $avail=$maxcat-$this->numcattickets[$catid]; + if($num>$avail){ + $ticket->setstatus(WOCartTicket::Exhausted); + $ticket->setstatustext(tr("Not enough tickets in this category.")); + $ticket->setmaxamount($avail); + return false; + } + //adjust numbers + $this->numtickets+=$num; + $this->numcattickets[$catid]+=$num; + //plan allows it + if($this->seatplan!==null){ + if(!$this->seatplan->addTickets($num,$catid)){ + $ticket->setstatus(WOCartTicket::Invalid); + $ticket->setstatustext(tr("Rejected by seat plan: ").$this->seatplan->lastError()); + return false; + }else + return true; + }else{ + return true; + } + } + + ///returns the event + public function getEvent() + { + return $this->event; + } + + ///returns the newly added ticket objects (array of WOCartTicket) + public function getTickets() + { + return $this->newtickets; + } +}; + +///This class verifies that the rules of a coupon are adhered to. +///Also used to execute those rules on the Cart before ordering. +class CouponVerifier +{ + private $couponid=null; + private $coupon=null; + + public function __construct($couponid) + { + $this->couponid=$couponid; + if($couponid!==null) + $this->coupon=WOCoupon::fromTablecoupon(WTcoupon::getFromDB($couponid)); + } + + public function isUseableOrNull() + { + if($this->coupon===null)return true; + //check time window + $now=time(); + if($this->coupon->getvalidfrom()!==null && $this->coupon->getvalidfrom()>$now)return false; + if($this->coupon->getvalidtill()!==null && $this->coupon->getvalidtill()<$now)return false; + //check access rights + global $session; + if(!$session->checkFlags($this->coupon->getflags()))return false; + //done, must be ok now + return true; + } + + public function verify($eventplan) + { + if(!$this->isUseableOrNull())return false; + if($this->coupon===null)return $this->verifyNull($eventplan); + //go through tickets + $vret=true; + foreach($eventplan->getTickets() as &$tick){ + $opc=$tick->getorigpricecategoryid(); + $pc=$tick->getpricecategoryid(); + //ignore if there is no move + if($opc===null || $opc==$pc)continue; + //find rule that matches the move + $found=false; + foreach($this->coupon->getrules() as $rule){ + if($rule->getfromcategory()==$opc && $rule->gettocategory()==$pc){ + $found=true; + break; + } + } + if(!$found){ + $vret=false; + $tick->setstatus(WOCartTicket::Invalid); + $tick->setstatustext(tr("Category conversion is not covered by the coupon.")); + } + } + //TODO: implement limits + //done + return $vret; + } + + private function verifyNull($eventplan) + { + $vret=true; + //make sure there are no altered categories + foreach($eventplan->getTickets() as $tick){ + $opc=$tick->getorigpricecategoryid(); + $pc=$tick->getpricecategoryid(); + if($opc!==null && $opc!=$pc){ + $vret=false; + $tick->setstatus(WOCartTicket::Invalid); + } + } + return $vret; + } +}; + + + +return; +?> diff --git a/www/inc/wext/autoload.php b/www/inc/wext/autoload.php index 3768d3e..f37356b 100644 --- a/www/inc/wext/autoload.php +++ b/www/inc/wext/autoload.php @@ -1,5 +1,5 @@ 0)$now+=$salestop*3600; - foreach($this->prop_tickets as &$tick){ + $coupon=new CouponVerifier($this->prop_couponid); + foreach($this->prop_tickets as $key => &$tick){ //assume ok $tick->setstatus(WOCartTicket::Ok); //check event exists $evid=$tick->geteventid(); - $ev=WTevent::getFromDB($evid); - if(!is_a($ev,"WTevent")){ + if(!isset($evplans[$evid])) + $evplans[$evid]=new EventPlan($evid); + if(!$evplans[$evid]->eventIsValid()){ $tick->setstatus(WOCartTicket::Invalid); $ret=false; continue; } - //check event flags - if(!$session->checkFlags($ev->flags)){ - $tick->setstatus(WOCartTicket::Invalid); - $ret=false; - continue; - } - //verify amount of seats in event - if(!array_key_exists($evid,$evseats)) - $evseats[$evid]=$this->eventTicketStatistics($evid,$ev->capacity,$evprice); - //verify price category, set price - $pcid=$tick->getpricecategoryid(); - if(!array_key_exists($pcid,$evseats[$evid])){ - $tick->setstatus(WOCartTicket::Invalid); - $ret=false; - continue; - } - //check that we have the right to sell this category for this event - if(!array_key_exists($evid,$pcat)) - $pcat[$evid]=array(); - if(!array_key_exists($pcid,$pcat[$evid])){ - $evprc=WOEventPrice::fromTableeventprice(WTeventprice::getFromDB($evid,$pcid)); - $pcat[$evid][$pcid]=$session->checkFlags($evprc->getflags()); - } - if(!$pcat[$evid][$pcid]){ - $tick->setstatus(WOCartTicket::Invalid); - $ret=false; - continue; - } - //set the price - $tick->setprice($evprice[$evid][$pcid]); - //check enough seats for event -// print_r($evseats);print_r($tick); - if($tick->getamount() > $evseats[$evid]["all"]){ - $tick->setstatus(WOCartTicket::Exhausted); - $tick->setmaxamount($evseats[$evid]["all"]); - $ret=false; - } - $evseats[$evid]["all"]-=$tick->getamount(); - //check enough seats for category - if($tick->getamount() > $evseats[$evid][$pcid]){ - $tick->setstatus(WOCartTicket::Exhausted); - $tick->setmaxamount($evseats[$evid][$pcid]); - $ret=false; - } -// print("......\n"); -// print_r($evseats);print_r($tick); - $evseats[$evid][$pcid]-=$tick->getamount(); + //try to add tickets + $ret &= $evplans[$evid]->addTickets($key, $tick); //check sale time if($salestop==self::AfterSale)continue; if($salestop==self::LateSale){ - if($now>$ev->endtime){ + if($evplans[$evid]->eventHasEnded($now)){ $tick->setstatus(WOCartTicket::TooLate); $ret=false; } }else{ - if($now>$ev->starttime){ + if($evplans[$evid]->eventHasStarted($now)){ $tick->setstatus(WOCartTicket::TooLate); $ret=false; } } } + foreach($evplans as $evid => $plan){ + //validate coupon + $ret &= $coupon->verify($plan); + } return $ret; } /**helper function for createOrder: verifies customer settings*/ private function verifyVouchers($trans,$vanyval,$vdiffprice) { - //TODO: implement $okvp=array(); foreach(explode(' ',$GLOBALS['db']->getConfig('ValidVouchers')) as $vp) $okvp[]=$vp+0; @@ -345,6 +303,7 @@ class WOCartOrder extends WOCartOrderAbstract $ord->ordertime=time(); $ord->comments=$this->prop_comment; $ord->amountpaid=0; + $ord->couponid=$this->prop_couponid; $ord->shippingtype=$this->prop_shippingtypeid; $ship=WTshipping::getFromDB($this->prop_shippingtypeid); if(is_a($ship,"WTshipping")) diff --git a/www/inc/wext/event.php b/www/inc/wext/event.php index 2603ad6..856dc39 100644 --- a/www/inc/wext/event.php +++ b/www/inc/wext/event.php @@ -144,6 +144,8 @@ class WOEvent extends WOEventAbstract } //copy stuff $evt->toTableevent($tab); + //TODO: fix bug in this... + //if($evt->getseatplanid()===null)$tab->seatplanid=null;//toTable.. does not handle null correctly for this case //check for cancel if($tab->isColumnChanged("iscancelled") && $tab->iscancelled){ if($trans->havePrivilege(WtrChangeEvent::Priv_CancelEvent)){ diff --git a/www/inc/wext/seatplan.php b/www/inc/wext/seatplan.php new file mode 100644 index 0000000..f1d24ae --- /dev/null +++ b/www/inc/wext/seatplan.php @@ -0,0 +1,281 @@ +categories=WTpricecategory::selectFromDB(""); + } + + ///initializes additional properties of groups and aligns with the event + public function initGroups($eventid) + { + global $db; + $this->eventprices=WTeventprice::selectFromDB("eventid=".$db->escapeInt($eventid)); + foreach($this->prop_Group as $g)$g->initGroup($this); + foreach($this->prop_VGroup as $g)$g->initGroup($this); + } + + ///finds a group by its ID/abbreviation or null if it cannot be found + public function groupById($abbr) + { + foreach($this->prop_Group as $g) + if($g->getid()==$abbr) + return $g; + return null; + } + + ///\internal helper for group: converts price string into category IDs + public function priceIDs($prc) + { + $ret=array(); + foreach(explode(' ',$prc) as $p) + { + $i=$p+0; + if(is_numeric($p) && is_int($i)){ + foreach($this->categories as $cat){ + if($cat->pricecategoryid==$i){ + $ret[]=$i; + break; + } + } + }else{ + foreach($this->categories as $cat){ + if($cat->abbreviation==$p){ + $ret[]=$cat->pricecategoryid; + break; + } + } + } + } + return $ret; + } + + ///\internal helper for group: converts price string into category IDs + public function priceAbbrs($prc) + { + $ret=array(); + foreach(explode(' ',$prc) as $p) + { + $i=$p+0; + if(is_numeric($p) && is_int($i)){ + foreach($this->categories as $cat){ + if($cat->pricecategoryid==$i){ + $ret[]=$cat->abbreviation; + break; + } + } + }else{ + foreach($this->categories as $cat){ + if($cat->abbreviation==$p){ + $ret[]=$cat->abbreviation; + break; + } + } + } + } + return $ret; + } + + ///\internal helper for group: get category capacities + public function priceCapacities($prc) + { + $ids=$this->priceIDs($prc); + $ret=array(); + foreach($this->eventprices as $ep) + { + if(in_array($ep->pricecategoryid,$ids)){ + $ret[]=$ep->maxavailable; + } + } + return $ret; + } + + ///tries to add tickets in a specific category to the corresponding group(s) + ///returns true on success, false if not all tickets could be added + ///in case of error the internal state may be inconsistent, so subsequent calls to this method are not reliable + //TODO: handle seat numbers + public function addTickets($numtickets,$categoryid) + { + //validate VGroups + foreach($this->prop_VGroup as $vg){ + if(in_array($categoryid,$vg->priceIDs())){ + if($vg->availableSeats()<$numtickets){ + $this->lasterror=tr("VGroup violated - ").$vg->getname(); + return false; + }else + $vg->addTickets($numtickets); + } + } + + //distribute over Groups + $catfound=false; + $cats=array(); + foreach($this->prop_Group as $gr){ + if(in_array($categoryid,$gr->priceIDs())){ + $catfound=true; + $cats[]=$gr->getname(); + $a=$gr->availableSeats(); + if($a<$numtickets){ + $gr->addTickets($a); + $numtickets-=$a; + }else{ + $gr->addTickets($numtickets); + $numtickets=0; + break; + } + } + } + + //handle exclusive==false + if($this->prop_exclusive==false && !$catfound)return true; + //anything left? + if($numtickets == 0) + return true; + else{ + $this->lasterror=tr("Not enough seats in groups ").implode(", ",$cats); + return false; + } + } + + public function lastError(){return $this->lasterror;} +}; + +class WOSeatPlanGroup extends WOSeatPlanGroupAbstract +{ + private $owner=null; + private $maxseats=0; + private $blockseats=0; + + ///\internal used by WOSeatPlan only + public function initGroup($p) + { + $this->owner=$p; + if(is_numeric($this->prop_capacity) && is_int($this->prop_capacity+0)) + $this->maxseats=$this->prop_capacity+0; + else switch($this->prop_capacity){ + case "maxcat":$this->maxseats=max($this->owner->priceCapacities($this->prop_price));break; + case "mincat":$this->maxseats=min($this->owner->priceCapacities($this->prop_price));break; + case "sumcat":foreach($this->owner->priceCapacities($this->prop_price) as $c)$this->maxseats+=$c;break; + case "auto"://TODO: count seats in rows + default:$this->maxseats=0; + } + if($this->maxseats<0)$this->maxseats=0; + } + + ///returns the categories as IDs + public function priceIDs() + { + return $this->owner->priceIDs($this->prop_price); + } + + ///returns the categories as Abbreviations + public function priceAbbrs() + { + return $this->owner->priceAbbrs($this->prop_price); + } + + ///returns the maximum of seats in this group + public function maxSeats() + { + return $this->maxseats; + } + + ///returns the number of currently blocked seats in this group + public function blockedSeats() + { + return $this->blockseats; + } + + ///returns the number of available seats in this group + public function availableSeats() + { + if($this->blockseats>=$this->maxseats)return 0; + if($this->blockseats<=0)return $this->maxseats; + return $this->maxseats - $this->blockseats; + } + + ///adds a number of tickets to the group + //TODO: handle seat numbers + public function addTickets($i) + { + if($i>0)$this->blockseats+=$i; + } +}; + +class WOSeatPlanVGroup extends WOSeatPlanVGroupAbstract +{ + private $owner=null; + private $maxseats=0; + private $blockseats=0; + + ///\internal used by WOSeatPlan only + public function initGroup($p) + { + $this->owner=$p; + if(is_numeric($this->prop_capacity) && is_int($this->prop_capacity+0)) + $maxseats=$this->prop_capacity+0; + else switch($this->prop_capacity){ + case "maxcat":$this->maxseats=max($this->owner->priceCapacities($this->prop_price));break; + case "mincat":$this->maxseats=min($this->owner->priceCapacities($this->prop_price));break; + case "sumcat":foreach($this->owner->priceCapacities($this->prop_price) as $c)$this->maxseats+=$c;break; + default:$this->maxseats=0; + } + if($this->maxseats<0)$this->maxseats=0; + } + + ///returns the categories as IDs + public function priceIDs() + { + return $this->owner->priceIDs($this->prop_price); + } + + ///returns the categories as Abbreviations + public function priceAbbrs() + { + return $this->owner->priceAbbrs($this->prop_price); + } + + ///returns the maximum of seats in this group + public function maxSeats() + { + return $this->maxseats; + } + + ///returns the number of currently blocked seats in this group + public function blockedSeats() + { + return $this->blockseats; + } + + ///returns the number of available seats in this group + public function availableSeats() + { + if($this->blockseats>=$this->maxseats)return 0; + if($this->blockseats<=0)return $this->maxseats; + return $this->maxseats - $this->blockseats; + } + + ///adds a number of tickets to the group + public function addTickets($i) + { + if($i>0)$this->blockseats+=$i; + } +}; + +class WOSeatPlanDefPrice extends WOSeatPlanDefPriceAbstract +{}; + + + +return; +?> diff --git a/www/inc/wext/ticket.php b/www/inc/wext/ticket.php index f121b5b..20db8f3 100644 --- a/www/inc/wext/ticket.php +++ b/www/inc/wext/ticket.php @@ -15,7 +15,13 @@ class WOTicket extends WOTicketAbstract else return 0; } - + + ///returns true if the ticket actually blocks a seat (reserved, ordered, in use, etc.) + public function isBlocking() + { + return ($this->prop_status & self::MaskBlock) != 0; + } + /**called from the UseTicket transaction*/ public static function useTicket($trans) { -- 1.7.2.5