[vox-tech] Sales/Shipping/Payment without Transactions.

Mike Simons vox-tech@lists.lugod.org
Fri, 25 Apr 2003 21:09:02 -0400


--tNQTSEo8WG/FKZ8E
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable

On Fri, Apr 25, 2003 at 09:26:44AM -0700, Micah J. Cowan wrote:
> Answer the question: what would you do to avoid having either an
> order in the DB marked to be shipped without payment, or payment for
> an order which didn't get into your DB?

  I will attempt to give an example, I'm not saying this is the only=20
way to do this without transactions.  This may be a bit long winded.

  Point out problems if you see them.

    Later,
      Mike Simons

  To keep this "simple", no transactions, no locking, many definitions,
I focus on only the shipping part of the question and simply payment=20
types to only credit cards.

  The samples below target mysql, and have a perl-ish syntax to them...=20
but syntax is not correct.

  There is one requirement: when an insert or update statement is run it=20
*must* hit the disk before any other insert or update statements that
come after it.  Said another way, if the database stores things out of
order you have a problem.


table definitions:
=3D=3D=3D
CREATE TABLE Customers (
  c_id INT UNSIGNED NOT NULL AUTO_INCREMENT,   # customer id
  c_first VARCHAR(20) NOT NULL,                # customer first name
  c_last VARCHAR(20) NOT NULL                  # customer last name
);

CREATE TABLE Items (
  i_id INT UNSIGNED NOT NULL AUTO_INCREMENT,   # item id
  i_name VARCHAR(20) NOT NULL,                 # item name
  i_price DOUBLE(16,2) NOT NULL                # item price
);

CREATE TABLE Orders (
  o_id INT UNSIGNED NOT NULL AUTO_INCREMENT,   # order id
  o_time TIMESTAMP NOT NULL,                   # timestamp
  o_c_id INT UNSIGNED NOT NULL,                # customer id
  o_p_id INT UNSIGNED NULL                     # payment id
  p_status ENUM ('creating', 'rejected', 'pending', 'shipped')
                                               # order processing status
);

CREATE TABLE OrderedItems (
  oi_o_id INT UNSIGNED NOT NULL,               # order id
  oi_i_id INT UNSIGNED NOT NULL,               # item id
  oi_count INT UNSIGNED NOT NULL,              # how many items
);

CREATE TABLE Payments (
  p_id INT UNSIGNED NOT NULL AUTO_INCREMENT,   # payment id
  p_time TIMESTAMP NOT NULL,                   # timestamp
  p_ccn INT(16) NOT NULL,                      # credit card number
  p_expr INT(4) NOT NULL,                      # expiration date
  p_auth INT(6) NULL                           # authorization number
  p_status ENUM ('checking', 'rejected', 'approved', 'done') # approval sta=
tus
);
=3D=3D=3D


sample transaction:
=3D=3D=3D
# Get the Customer ID...
  select c_id from Customers where (c_first =3D 'John' && c_last =3D 'Doe');
  $customer_id =3D $c_id
or...
  insert into Customers values (NULL, 'John', 'Doe');
  $customer_id =3D $mysql_insertid


# Start an order
insert into Orders values (NULL, NULL, c_id, NULL, 'creating');
$order_id =3D $mysql_insertid


# Add items into ordered Items
for each item in the list to order
  insert into OrderedItems values ($order_id, $item_type, $how_many);


# Start payment authorization
insert into Payments values=20
  (NULL, NULL, $card_number, $card_expire, NULL, 'checking');
$payment_id =3D $mysql_insertid

($auth_number, #status) =3D get_credit_auth();  # payment authorization
# status is either 'rejected' or 'approved'

#### race condition (1)

# Finish payment authorization
update Payments set p_auth=3D$auth_number, p_status=3D$status
  where (p_id =3D $payment_id);

#### race condition (2)

# Finish order
if ($status not equal 'approved') {
  update Orders set p_id=3D$payment_id,o_status=3D'rejected'
    where (o_id =3D $order_id);

  print "your payment has been rejected";
} else {
  update Orders set p_id=3D$payment_id,o_status=3D'pending'
    where (o_id =3D $order_id);

  print "your authorization number is $auth_number";
}
=3D=3D=3D


logic:
=3D=3D=3D
to find orders that are being entered right now:
  select o_id from Orders where o_status =3D 'creating';

to find orders that were rejected for credit reasons:
  select o_id from Orders where o_status =3D 'rejected';

to find orders that need product shipped:
  select o_id from Orders where o_status =3D 'pending';


to find which payments need to be billed to the credit card company:
  select p_id from Payments where p_status =3D 'approved';


to find out which transactions are "stale" or left over from
  an application crash, look for order status 'creating' or=20
  payment status 'checking'... at a time when the system is=20
  offline or after the time stamp is X days old.
=3D=3D=3D


Race Condition 1:
=3D=3D=3D=3D
  Credit card company issued a authorization, but the authorization
number is not stored.

  Since the payment status never changes to 'approved', order status
never moves to 'pending', and items are not shipped.
  Even though a credit card authorization is issued I expect the customer
is not billed.  I have not dealt with credit card transactions before, but=
=20
here is why I suspect that this is fine.

  In my mind the authorization step just puts a short term hold on the
funds, the item_selling_company must *bill* the credit card company
with authorization number to actually collect money.  This is why some
transactions clear on one's credit card statement days away from when
the store receipt says.
=3D=3D=3D=3D

Race Condition 2:
=3D=3D=3D=3D
  Credit authorization is stored, but customer is not informed that
the transaction is complete.
 =20
  The items will ship to the customer.  The only concern is they may
redo the transaction and order a second shipment... this is out of
scope.

  The application could make it easy for the user to check the status=20
of they most recent order.
=3D=3D=3D=3D

  I think a system like the one above would be very easy to track down
problems in, at each important step a log is made.

  Instead of using status ENUM columns like I did above or if you were
working with a DB that doesn't support them.  You can do null-able=20
timestamps one for each order state:
  Created
  Approved
  Shipped
and for each payment state:
  Requested
  Approved      (set to a not-null known value for a reject)
  Billed

--=20
GPG key: http://simons-clan.com/~msimons/gpg/msimons.asc
Fingerprint: 524D A726 77CB 62C9 4D56  8109 E10C 249F B7FA ACBE

--tNQTSEo8WG/FKZ8E
Content-Type: application/pgp-signature
Content-Disposition: inline

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.0.6 (GNU/Linux)
Comment: For info see http://www.gnupg.org

iD8DBQE+qdwu4Qwkn7f6rL4RAt2dAJ4wK/7XeOphiT72PHZaL8i5bEdo6ACeLP44
OR24B7vP5yIJHObgshZ6CoY=
=jozw
-----END PGP SIGNATURE-----

--tNQTSEo8WG/FKZ8E--