package divconq.cms.service; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.concurrent.atomic.AtomicReference; import org.joda.time.DateTime; import divconq.db.ObjectResult; import divconq.db.query.CollectorField; import divconq.db.query.SelectDirectRequest; import divconq.db.query.SelectFields; import divconq.db.query.WhereField; import divconq.db.query.WhereNotEqual; import divconq.db.update.DbRecordRequest; import divconq.db.update.InsertRecordRequest; import divconq.db.update.UpdateRecordRequest; import divconq.hub.Hub; import divconq.interchange.authorize.AuthUtil; import divconq.lang.CountDownCallback; import divconq.lang.op.FuncCallback; import divconq.lang.op.OperationCallback; import divconq.lang.op.OperationContext; import divconq.lang.op.UserContext; import divconq.struct.CompositeStruct; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.struct.Struct; import divconq.util.StringUtil; import divconq.xml.XElement; public class OrderUtil { static public void processAuthOrder(RecordStruct order, FuncCallback<String> callback) { OrderUtil.santitizeAndCalculateOrder(order, new OperationCallback() { @Override public void callback() { OperationContext.get().touch(); if (this.hasErrors()) { callback.complete(); return; } DateTime now = new DateTime(); RecordStruct orderclean = (RecordStruct) order.deepCopy(); // remove sensitive information before saving RecordStruct cleanpay = orderclean.getFieldAsRecord("PaymentInfo"); if (cleanpay != null) { cleanpay.removeField("CardNumber"); cleanpay.removeField("Expiration"); cleanpay.removeField("Code"); } // insert the order DbRecordRequest req = new InsertRecordRequest() .withTable("dcmOrder") .withSetField("dcmOrderDate", now) .withSetField("dcmStatus", "AwaitingPayment") .withSetField("dcmLastStatusDate", now) .withSetField("dcmOrderInfo", orderclean) .withSetField("dcmGrandTotal", order.getFieldAsRecord("CalcInfo").getFieldAsDecimal("GrandTotal")); UserContext uctx = OperationContext.get().getUserContext(); // if this is an authenticated user then we want to track the customer id too if (uctx.isAuthenticated()) req.withSetField("dcmCustomer", uctx.getUserId()); Hub.instance.getDatabase().submit(req, new ObjectResult() { @Override public void process(CompositeStruct result) { OperationContext.get().touch(); if (this.hasErrors()) { callback.complete(); return; } RecordStruct resultrec = (RecordStruct) result; String refid = resultrec.getFieldAsString("Id"); callback.setResult(refid); boolean testing = Hub.instance.getResources().isForTesting(); // Struct.objectToBooleanOrFalse(Hub.instance.getConfig().getAttribute("ForTesting")); // TODO lookup user and see if they are in "test" mode - this way some people can run test orders through system XElement dsettings = OperationContext.get().getDomain().getSettings(); XElement sset = dsettings.find("Store"); if (sset == null) { callback.error("Missing store settings."); callback.complete(); return; } if (sset.hasAttribute("Mode")) testing = "Dev".equals(sset.getAttribute("Mode")); XElement auth = sset.selectFirst(testing ? "AuthorizeDev" : "AuthorizeLive"); if (auth == null) { callback.error("Missing store Authorize settings."); callback.complete(); return; } String lid = auth.getAttribute("LoginId"); String key = auth.getAttribute("TransactionKey"); String authid = Hub.instance.getClock().getObfuscator().decryptHexToString(lid); String authkey = Hub.instance.getClock().getObfuscator().decryptHexToString(key); // TODO store order items as independent records? order audits? other fields/tables to fill in? // put order into a thread and box AuthUtil.authXCard(authid, authkey, refid, !testing, false, order, new FuncCallback<RecordStruct>() { @Override public void callback() { OperationContext.get().touch(); DbRecordRequest upreq = new UpdateRecordRequest() .withId(refid) .withTable("dcmOrder"); if (this.hasErrors() || this.isEmptyResult()) upreq.withSetField("dcmStatus", "VerificationRequired"); else upreq.withSetField("dcmStatus", "AwaitingFulfillment"); if (this.isEmptyResult()) upreq.withSetField("dcmPaymentResponse", this.toLogMessage()); else upreq.withSetField("dcmPaymentId", this.getResult().getFieldAsString("TxId")) .withSetField("dcmPaymentResponse", this.getResult().getFieldAsString("Message")); Hub.instance.getDatabase().submit(upreq, new ObjectResult() { @Override public void process(CompositeStruct result) { OperationContext.get().touch(); // TODO mark coupons used if present /* <Field Name="dcmWasUsed" Type="Boolean" /> <Field Name="dcmAmountUsed" Type="Decimal" /> */ callback.complete(); } }); } }); } }); } }); } static public void santitizeAndCalculateOrder(RecordStruct order, OperationCallback callback) { // ------------------------------------------- // be sure that the customer info is good RecordStruct custinfo = order.getFieldAsRecord("CustomerInfo"); // required UserContext uctx = OperationContext.get().getUserContext(); // if this is an authenticated user then we want to track the customer id too if (uctx.isAuthenticated()) custinfo.setField("CustomerId", uctx.getUserId()); else custinfo.removeField("CustomerId"); // ------------------------------------------- // check products are real and priced right ListStruct items = order.getFieldAsList("Items"); ListStruct pidlist = new ListStruct(); //ListStruct remlist = new ListStruct(); for (Struct itm : items.getItems()) pidlist.addItem(((RecordStruct) itm).getFieldAsString("Product")); // grab weight / ship cost from here but don't store in order info SelectDirectRequest req = new SelectDirectRequest() .withTable("dcmProduct") .withSelect(new SelectFields() .withField("Id", "Product") .withField("dcmTitle", "Title") .withField("dcmAlias", "Alias") .withField("dcmSku", "Sku") .withField("dcmDescription", "Description") .withField("dcmTag", "Tags") .withField("dcmVariablePrice", "VariablePrice") .withField("dcmMininumPrice", "MininumPrice") .withField("dcmPrice", "Price") .withField("dcmSalePrice", "SalePrice") .withField("dcmTaxFree", "TaxFree") .withField("dcmShipFree", "ShipFree") .withField("dcmShipAmount", "ShipAmount") .withField("dcmShipWeight", "ShipWeight") .withForeignField("dcmCategory", "CatShipAmount", "dcmShipAmount") ) .withCollector(new CollectorField("Id").withValues(pidlist)) .withWhere(new WhereNotEqual(new WhereField("dcmDisabled"), true)); // do search Hub.instance.getDatabase().submit(req, new ObjectResult() { @Override public void process(CompositeStruct result) { OperationContext.get().touch(); if (this.hasErrors()) { callback.complete(); return; } XElement dsettings = OperationContext.get().getDomain().getSettings(); AtomicReference<BigDecimal> itmcalc = new AtomicReference<>(BigDecimal.ZERO); AtomicReference<BigDecimal> taxcalc = new AtomicReference<>(BigDecimal.ZERO); AtomicReference<BigDecimal> shipcalc = new AtomicReference<>(BigDecimal.ZERO); AtomicReference<BigDecimal> itemshipcalc = new AtomicReference<>(BigDecimal.ZERO); AtomicReference<BigDecimal> catshipcalc = new AtomicReference<>(BigDecimal.ZERO); AtomicReference<BigDecimal> itemshipweight = new AtomicReference<>(BigDecimal.ZERO); // TODO look up shipping AtomicReference<BigDecimal> shipamt = new AtomicReference<>(BigDecimal.ZERO); // TODO look up coupons, check them and apply them AtomicReference<BigDecimal> itmdiscount = new AtomicReference<>(BigDecimal.ZERO); AtomicReference<BigDecimal> shipdiscount = new AtomicReference<>(BigDecimal.ZERO); // if we find items from submitted list in the database then add those items to our true item list ListStruct fnditems = new ListStruct(); // loop our items for (Struct itm : items.getItems()) { RecordStruct orgitem = (RecordStruct) itm; for (Struct match : ((ListStruct)result).getItems()) { RecordStruct rec = (RecordStruct) match; if (!rec.getFieldAsString("Product").equals(orgitem.getFieldAsString("Product"))) continue; RecordStruct item = (RecordStruct) orgitem.deepCopy(); // make sure we are using the 'real' values here, from the DB // we are excluding fields that are not valid in an order item, however these fields are still used in the calculation below item.copyFields(rec, "ShipAmount", "ShipWeight", "CatShipAmount"); // add to the 'real' order list fnditems.addItem(item); BigDecimal price = item.isFieldEmpty("SalePrice") ? item.getFieldAsDecimal("Price", BigDecimal.ZERO) : item.getFieldAsDecimal("SalePrice"); if (rec.getFieldAsBooleanOrFalse("VariablePrice")) { price = orgitem.getFieldAsDecimal("Price", BigDecimal.ZERO); BigDecimal min = rec.getFieldAsDecimal("MininumPrice", BigDecimal.ZERO); if (price.compareTo(min) < 0) price = min; // sale price not appropriate on variable priced items item.setField("Price", price); item.removeField("SalePrice"); } BigDecimal qty = item.getFieldAsDecimal("Quantity", BigDecimal.ZERO); BigDecimal total = price.multiply(qty); item.withField("Total", total); itmcalc.set(itmcalc.get().add(total)); if (!item.getFieldAsBooleanOrFalse("ShipFree")) shipcalc.set(shipcalc.get().add(total)); if (!item.getFieldAsBooleanOrFalse("TaxFree")) taxcalc.set(taxcalc.get().add(total)); if (!rec.isFieldEmpty("ShipAmount")) itemshipcalc.set(itemshipcalc.get().add(rec.getFieldAsDecimal("ShipAmount").multiply(qty))); if (!rec.isFieldEmpty("ShipWeight")) itemshipweight.set(itemshipweight.get().add(rec.getFieldAsDecimal("ShipWeight").multiply(qty))); if (!rec.isFieldEmpty("CatShipAmount")) catshipcalc.set(catshipcalc.get().add(rec.getFieldAsDecimal("CatShipAmount").multiply(qty))); break; } } // replace the proposed items with the found and cleaned items order.setField("Items", fnditems); /* Enum="Disabled,OrderWeight,OrderTotal,PerItem,PerItemFromCategory,Custom" */ XElement shipsettings = dsettings.selectFirst("Store/Shipping"); String shipmode = "Disabled"; // shipping is based on Order Total before discounts if (shipsettings != null) { shipmode = shipsettings.getAttribute("Mode", shipmode); // TODO if OrderWeight,OrderTotal then do a table lookup in shipsettings // TODO if custom then harass the domain watcher with a shipping calc if ("PerItem".equals(shipmode)) shipamt.set(itemshipcalc.get()); else if ("PerItemFromCategory".equals(shipmode)) shipamt.set(catshipcalc.get()); } CountDownCallback couponcd = new CountDownCallback(1, new OperationCallback() { @Override public void callback() { BigDecimal itmtotal = itmcalc.get().add(itmdiscount.get().negate()); if (itmtotal.stripTrailingZeros().compareTo(BigDecimal.ZERO) < 0) itmtotal = BigDecimal.ZERO; BigDecimal shiptotal = shipamt.get().add(shipdiscount.get().negate()); if (shiptotal.stripTrailingZeros().compareTo(BigDecimal.ZERO) < 0) shiptotal = BigDecimal.ZERO; // look up taxes BigDecimal taxat = BigDecimal.ZERO; XElement taxtable = dsettings.selectFirst("Store/TaxTable"); if (taxtable != null) { String state = null; RecordStruct shipinfo = order.getFieldAsRecord("ShippingInfo"); // not required if ((shipinfo != null) && !shipinfo.isFieldEmpty("State")) state = shipinfo.getFieldAsString("State"); if (StringUtil.isEmpty(state)) { RecordStruct billinfo = order.getFieldAsRecord("BillingInfo"); // not required if ((billinfo != null) && !billinfo.isFieldEmpty("State")) state = billinfo.getFieldAsString("State"); } if (StringUtil.isNotEmpty(state)) { for (XElement stel : taxtable.selectAll("State")) { if (state.equals(stel.getAttribute("Alias"))) { taxat = new BigDecimal(stel.getAttribute("Rate", "0.0")); break; } } } } // TODO account for product discounts in taxcalc, apply discounts to the taxfree part first then reduce taxcalc by any remaining discount amt BigDecimal taxtotal = taxcalc.get().multiply(taxat).setScale(2, RoundingMode.HALF_EVEN); // correct order calculations, totals RecordStruct calcinfo = new RecordStruct() .withField("ItemCalc", itmcalc.get()) .withField("ProductDiscount", itmdiscount.get()) .withField("ItemTotal", itmtotal) .withField("ShipCalc", shipcalc.get()) .withField("ShipAmount", shipamt.get()) .withField("ShipDiscount", shipdiscount.get()) .withField("ShipTotal", shiptotal) .withField("TaxCalc", taxcalc.get()) .withField("TaxAt", taxat) .withField("TaxTotal", taxtotal) .withField("GrandTotal", itmtotal.add(shiptotal).add(taxtotal)); order.setField("CalcInfo", calcinfo); callback.complete(); } }); if (order.isFieldEmpty("CouponCode")) { couponcd.countDown(); return; } // TODO add discount support couponcd.countDown(); return; /* // grab weight / ship cost from here but don't store in order info SelectDirectRequest req2 = new SelectDirectRequest() .withTable("dcmDiscount") .withSelect(new SelectFields() .withField("Id") .withField("dcmTitle", "Title") .withField("dcmCode", "Code") .withField("dcmMode", "Mode") .withField("dcmAmount", "Amount") .withField("dcmMinimumOrder", "MinimumOrder") .withField("dcmStart", "Start") .withField("dcmExpire", "Expire") .withField("dcmOneTimeUse", "OneTimeUse") .withField("dcmWasUsed", "WasUsed") ) .withCollector(new CollectorField("dcmCode").withValues(order.getFieldAsString("CouponCode"))); // do search Hub.instance.getDatabase().submit(req2, new ObjectResult() { @Override public void process(CompositeStruct result) { OperationContext.get().touch(); if (this.hasErrors()) { callback.complete(); return; } if ((result == null) || (((ListStruct) result).getSize() == 0)) { couponcd.countDown(); return; } /* TODO check all this and the order modes too * calc off of order create date for star/expire .withField("dcmMinimumOrder", "MinimumOrder") .withField("dcmStart", "Start") .withField("dcmExpire", "Expire") * / RecordStruct crec = ((ListStruct) result).getItemAsRecord(0); if (crec.getFieldAsBooleanOrFalse("OneTimeUse") && crec.getFieldAsBooleanOrFalse("WasUsed")) { couponcd.countDown(); return; } ListStruct disclist = new ListStruct(); // TODO support other modes - FixedOffTotal,FixedOffProduct,PercentOffProduct,FixedOffShipping,PercentOffShipping,FlatShipping,FreeShipping String mode = crec.getFieldAsString("Mode"); if ("FixedOffTotal".equals(mode)) { if (!crec.isFieldEmpty("Amount")) { itmdiscount.set(crec.getFieldAsDecimal("Amount")); // TODO off total not off prod } } disclist.addItem(new RecordStruct() .withField("Id", crec.getFieldAsString("Id")) .withField("Code", order.getFieldAsString("CouponCode")) .withField("Title", crec.getFieldAsString("Title")) .withField("Amount", itmdiscount.get()) // TODO or ship disc ); order.setField("Discounts", disclist); couponcd.countDown(); } }); */ } }); } }