* some existing temp or test directory of your choice, you might use GETENV("TEMP")
#Define ccBaseDir "D:\test\"
Close Tables all
Close Databases all
Erase (ccBaseDir+"Warehouse.*")
Erase (ccBaseDir+"Carts.*")
Erase (ccBaseDir+"Sessions.*")
Create Database (ccBaseDir+"Warehouse")
Create Table (ccBaseDir+"Warehouse");
(iId I AutoInc, cName C(10), iStock I, iAllocated I Default 0)
Create Table (ccBaseDir+"Carts");
(iId I AutoInc, iSessionID I, iComponentID I, iAmount I)
Create Table (ccBaseDir+"Sessions");
(iID I AutoInc, cStartedBy C(50), tStartedAt T Default Datetime())
Close Tables All
* Warehouse, representing all your warehouse stock
Insert Into Warehouse (cName, iStock) Values ("Nuts",200)
Insert Into Warehouse (cName, iStock) Values ("Bolts",200)
* Start a Shopping Cart Session (order processing from a clerk)
Local lnMySessionID, llSuccess
Insert Into Sessions (cStartedBy) Values (Id())
lnMySessionID = Sessions.iID
* Bill of Materials, representing detailed items needed for a product or order of several products
Create Cursor crsBillOfMaterials (iID I autoinc, iComponentID I, iNeededAmount I)
*Bill of Materials: 12 Nuts and 12 Bolts...
Insert Into crsBillOfMaterials (iComponentID, iNeededAmount) Values (1,12) && ID 1 for nuts
Insert Into crsBillOfMaterials (iComponentID, iNeededAmount) Values (2,12) && ID 2 for bolts
* Bill of Materials is what you want to put into your shopping cart.
* First, virtually take from the warehouse shelves:
Update Warehouse ;
Set iStock = iStock - iNeededAmount ;
, iAllocated = iAllocated + iNeededAmount ;
From crsBillOfMaterials ;
Where Warehouse.iID = crsBillOfMaterials.iComponentID
* This database change is "non desctructive". No information is destroyed or depends on
* the current state of the clients memory. The sum iStock+iAllocated is kept constant.
* In case of a crash right now we still know the real iStock.
* This database change is atomic, it happens in an instant with automatic lock.
* No further manual lock or transaction is needed, as this all takes place in a single table.
* Other clerks order processing can be done in parallel.
* Second, check if you took out more than available:
Select Warehouse.iID, iStock ;
From Warehouse ;
Inner Join crsBillOfMaterials ;
On Warehouse.iID = crsBillOfMaterials.iComponentID ;
Where Warehouse.iStock<0;
Into Cursor crsMissingStock
llSuccess = (_Tally=0)
* crsMissingStock includes any insufficient supply of components also due to other clerks
* concurrent activity even including parallel virtually taken or put back items.
* That means: If reading back current iStock values <0 reveals no record, there is no problem,
* which includes the luck a clerk was putting back items right at the time we needed them
* or there was sufficient stock for two or more currently processed bills of materials.
If NOT m.llSuccess
* Insufficient stock, put back virtually taken items instead of putting them into a cart:
Update Warehouse ;
From crsBillOfMaterials ;
Set iStock = iStock + iNeededAmount ;
, iAllocated = iAllocated - iNeededAmount ;
Where Warehouse.iID = crsBillOfMaterials.iComponentID
* Putting the virtually taken items back immediately helps ohter clerks to see real
* stock values. Again - a non destructive, atomic, automatically locked database change.
* This update resembles a Rollback, but also is correct in case other clerks worked on
* warehouse data in the meantime, so it's even better than a transaction, as it doesn't
* lock out anybody.
* Display the message afterwards:
MessageBox("Insufficient Stock.",64+0,"Info for the clerk")
* You might also display crsMissingStock now
Else && Success!
* Start cart processing
* We put the whole BillOfMaterials into the current session cart:
Insert Into Carts (iSessionID, iComponentID, iAmount);
Select m.lnMySessionID ;
, iComponentID ;
, iNeededAmount ;
From crsBillOfMaterials
* This now reflects where iAllocated counts went to
* Putting bill of materials into the cart could go into a transaction together with the
* initial virtual warehouse update and the select checking stock, but that would block out
* others too early.
* There is no need to worry about crsBillOfMaterials data change. We put same things into
* the cart as we removed from the warehouse. The crsBillOfMaterials is the leading data for
* that information anyway.
* Warehouse data is always intact, as the iAllocated count accounts for not yet really removed
* stock. The information about the allocation now just isn't anonymous anymore. We just might
* need to rethink that, if it's stretched out into several UI forms, but that makes a transaction
* even less possible.
* Finally: It's a plus we don't needed a transaction, this means more in parallel processing
* possibilities, as unlikely the timely concurrence of these processing may be.
* Next step:
* Depending on a final clerk decision the cart content goes back to the warehouse or is
* sold (which could mean forwarding to product assembly or packaging and delivery)
lnAnswer = MessageBox("Sufficient Stock. Sell?",32+4,"Question for the clerk")
Local lnInitialTransactionLevel
lnInitialTransactionLevel = Txnlevel()
Try
* Here we put the final change of Warehouse and Cart into one transaction:
Begin Transaction
If lnAnswer = 6 && Yes, we have a sell
Update Warehouse From Carts ;
Set iAllocated = iAllocated - Carts.iAmount;
Where Warehouse.iID = Carts.iComponentID;
And Carts.iSessionID = m.lnMySessionID
* Notice: Stock remains at its previously reduced value.
* Reducing iAllocated means this amount of items
* now is finally removed from warehouse stock.
* Again a non destructive, atomic, automatically locked database change
Else
* No sell, put the cart data back into warehouse stock
Update Warehouse From Carts ;
Set iStock = iStock + Carts.iAmount ;
, iAllocated = iAllocated - Carts.iAmount ;
Where Warehouse.iID = Carts.iComponentID ;
And Carts.iSessionID = m.lnMySessionID
* Again a non destructive, atomic, automatically locked database change
EndIf
* End cart processing
Delete From Carts Where iSessionID = m.lnMySessionID
End Transaction
* It's not very essential to have Warehouse and Cart changes in a transaction,
* because the anonymous iAllocated count is good enough to know at any time.
* Having further user/pc related cart data just helps finding out to whom and where
* a misfortune situation occurred.
Catch
* Something went wrong, perhaps at the hardware level
If Txnlevel()>lnInitialTransactionLevel
RollBack
Endif
EndTry
EndIf
* End session
Delete From Sessions Where iID = m.lnMySessionID
* You might set an end date instead of deleting, too.
* Worst case scenartio: A crash of the software at any line
* You will have not closed sessions, filled carts and Warehouse.iAllocated>0 to check at end
* of day database integrity checks, But there is no place in this code putting the database
* into a transient and incomplete state with important info about what other changes to make
* only existing at the client side. That's not the case.
* If any Warehouse.iAllocated is >0 you can mend data from putting back carts of todays
* unfninshed/undeleted sessions or just move iAllocated itself back to iStock.
* Even though there might be a crash inbetween adding to the cart and subtracting from
* warehouse carts are not important, if sum of all cart items differes from sum of all
* iAllocated this simply hints on crashes between the initial update and inserting into the
* Carts table, you just don't have the info on where the items were processed by whom, but
* iAllocated is reliable info on its own.
* You can deduct the client PC from it missing on other current sessions and carts.
* You may log such bad scenario data for further in depth analysis of what went wrong when
* and at which client PC with the help of Sessions records (including the deleted ones).