/// <summary> /// When inventory inbound is created, find the inventory item that needs to be fullfilled /// and fullfil with item from inventory inbound /// </summary> /// <param name="db"></param> /// <param name="obj"></param> internal static void ProcessInventoryInboundCreation(NancyBlackDatabase db, InventoryInbound obj) { // ensures that only one thread will be doing this lock (InventoryAdminModule.LockObject) { db.Transaction(() => { var inboundItems = new List <InventoryItem>(); foreach (var item in obj.Items) { InventoryItem ivitm = new InventoryItem(); ivitm.InboundDate = obj.InboundDate; ivitm.InventoryInboundId = obj.Id; ivitm.ProductId = item.ProductId; ivitm.BuyingCost = item.Price; ivitm.BuyingTax = item.Tax; db.UpsertRecord(ivitm); inboundItems.Add(ivitm); } InventoryAdminModule.InboundCompleted(db, obj, inboundItems); }); } }
private void UpdateTag_ObjectUpdate(NancyBlackDatabase db, string table, dynamic obj) { if (!(obj is IContent)) { return; } IContent content = obj; var type = content.GetType().Name; var existTags = db.Query <Tag>().Where(tag => tag.ContentId == content.Id && tag.Type == type); if (content.Tags != null) { var source = string.Join(",", content.Tags.Split(',').Distinct().OrderBy(s => s)).ToLowerInvariant(); var inDb = string.Join(",", existTags.ToList().Select(tag => tag.Name).OrderBy(s => s)).ToLowerInvariant(); // return when there is nothing change in tags if (source == inDb) { return; } } db.Transaction(() => { // delete exist tags for re-add new tags foreach (var tag in existTags) { db.DeleteRecord(tag); } // re-insert tag this.InsertTag(db, content); }); }
internal static void ProcessReceiptCreation(NancyBlackDatabase db, Receipt obj) { // When payment receipt is created, create accounting entry db.Transaction(() => { var saleorder = db.GetById <SaleOrder>(obj.SaleOrderId); var paymentlog = db.GetById <PaymentLog>(obj.PaymentLogId); if (saleorder == null || paymentlog == null) { // bogus receipt throw new InvalidOperationException("Invalid Receipt was created"); } // Ensures all sale order logic has been ran // if the sale order was created before new system change if (saleorder.__createdAt < TaxSystemEpoch) { saleorder.UpdateSaleOrder(AdminModule.ReadSiteSettings(), db, false); } // Receipt will create 2 entries // 1) PaymentSource account increases, with total amount // TODO: Mapping from PaymentSource to Account AccountingEntry entry1 = new AccountingEntry(); entry1.TransactionDate = paymentlog.__createdAt; entry1.TransactionType = "income"; entry1.DebtorLoanerName = "Customer"; entry1.IncreaseAccount = paymentlog.PaymentSource; entry1.IncreaseAmount = saleorder.TotalAmount; entry1.SaleOrderId = saleorder.Id; db.UpsertRecord(entry1); if (saleorder.TotalTax > 0) { // 2) paid tax is decreased // (ภาษีขาย ทำให้ภาษีซื้อลดลง, ภาษีซื้อ บันทึกไว้ตอน InventoryInbound) AccountingEntry entry2 = new AccountingEntry(); entry2.TransactionDate = paymentlog.__createdAt; entry2.TransactionType = "expense"; entry2.DebtorLoanerName = "Tax"; entry2.DecreaseAccount = "Paid Tax"; entry2.DecreaseAmount = saleorder.TotalTax * -1; entry2.SaleOrderId = saleorder.Id; db.UpsertRecord(entry2); } }); }
/// <summary> /// Store value to database, only dirty values are stored /// </summary> /// <param name="db"></param> public void Persist(NancyBlackDatabase db) { lock (this) { db.Transaction(() => { foreach (var key in _DirtyList) { db.UpsertRecord(_Variables[key]); } }); _DirtyList = new HashSet <string>(); } }
public static void SetPackingStatus(NancyBlackDatabase db, SaleOrder so) { // deduct stock of all product // first, we group the product id to minimize selects db.Transaction(() => { var products = from item in so.Items group item by item into g select g; foreach (var productIdGroup in products) { var product = db.GetById <Product>(productIdGroup.Key); product.Stock = product.Stock - productIdGroup.Count(); db.UpsertRecord <Product>(product); } }); }
/// <summary> /// Set Receipt Index /// </summary> /// <param name="db"></param> /// <param name="month"></param> public static void SetReceiptIdentifier(NancyBlackDatabase db, DateTime month) { db.Transaction(() => { // now, find all payment log of this month var startOfMonth = new DateTime(month.Year, month.Month, 1, 0, 0, 0, DateTimeKind.Utc); // this is temporary fix for receipt number to be in order as they created // because datetime in db are store in utc tick and we cannot use .ToLocalTime() in db.Query().Where() as it unsuported // so we use startOfMonth - thaiTimeZone (7 hours) instead var thaiTimeZone = TimeZoneInfo.FindSystemTimeZoneById("SE Asia Standard Time"); startOfMonth = startOfMonth.AddTicks(thaiTimeZone.BaseUtcOffset.Ticks * -1); var endOfMonth = startOfMonth.AddMonths(1).AddMilliseconds(-1); var paymentsThisMonth = db.Query <PaymentLog>() .Where(l => l.__createdAt >= startOfMonth && l.__createdAt <= endOfMonth) .OrderBy(l => l.Id).ToList(); int counter = 1; foreach (var l in paymentsThisMonth) { var receipt = db.Query <Receipt>().Where(r => r.PaymentLogId == l.Id).FirstOrDefault(); if (receipt == null) { // payment is not successful - so no receipt } else { if (string.IsNullOrWhiteSpace(receipt.Identifier)) { // our company is in Thailand so, we only publish doc in Thailand Time var receiptPublishedDate = l.__createdAt.ToUniversalTime().Add(thaiTimeZone.BaseUtcOffset); receipt.Identifier = receiptPublishedDate.ToString("RCyyyyMM-", System.Globalization.CultureInfo.InvariantCulture) + string.Format("{0:0000}", counter); db.UpsertRecord(receipt); } counter++; } } }); }
/// <summary> /// Claim the rewards /// </summary> /// <param name="db"></param> /// <param name="rewardsId"></param> /// <param name="registrationId"></param> /// <returns></returns> public static AffiliateRewardsClaim ClaimReward(NancyBlackDatabase db, int rewardsId, int registrationId) { var canClaim = AffiliateReward.CanClaim(db, rewardsId, registrationId); if (canClaim == false) { return(null); } AffiliateReward rewards; var reg = db.GetById <AffiliateRegistration>(registrationId); rewards = db.GetById <AffiliateReward>(rewardsId); if (rewards.MaxPerUser > 0) { lock (BaseModule.GetLockObject("RewardClaim-Reg-" + registrationId)) { var totalClaimedByUser = db.Query <AffiliateRewardsClaim>() .Where(c => c.AffiliateRewardsId == rewards.Id && c.AffiliateRegistrationId == registrationId).Count(); if (totalClaimedByUser >= rewards.MaxPerUser) { return(null); } } } if (rewards.TotalQuota > 0) { lock (BaseModule.GetLockObject("RewardClaim-" + rewardsId)) { var totalClaimed = db.Query <AffiliateRewardsClaim>().Where(c => c.AffiliateRewardsId == rewards.Id).Count(); rewards.RemainingQuota = rewards.TotalQuota - totalClaimed; db.UpsertRecord(rewards); } } if (rewards.IsRewardsClaimable == false) { return(null); } if (rewards.IsCodeDiscount || rewards.IsFreeGiftInSaleOrder) { var until = DateTime.MaxValue.Ticks; if (rewards.CodeDicountExpiryInDays != null) { until = DateTime.Now.AddDays(rewards.CodeDicountExpiryInDays.Value).Ticks; } if (rewards.CodeDiscountExpiryDate != null) { until = rewards.CodeDiscountExpiryDate.Value.Ticks; } AffiliateRewardsClaim claim = null; db.Transaction(() => { // free gift also gets created as code Product p = new Product(); if (rewards.IsCodeDiscount) { p.Price = rewards.CodeDiscountAmount * -1; p.Attributes = new { rewardId = rewards.Id, description = rewards.Title + ", ราคาก่อนส่วนลดขั้นต่ำ: " + rewards.MinimumPurchaseAmount, min = rewards.MinimumPurchaseAmount, onetime = true, until = until, discount = rewards.CodeDiscountAmount, affiliateName = reg.AffiliateName, require = rewards.RequiredProductIds, }; } if (rewards.IsFreeGiftInSaleOrder) { p.DiscountPrice = 0; p.Price = rewards.CodeDiscountAmount; p.PromotionEndDate = new DateTime(until); p.MasterProductId = rewards.RewardsProductId; p.IsVariation = true; p.Attributes = new { rewardId = rewards.Id, description = rewards.Title + ", ราคาก่อนส่วนลดขั้นต่ำ: " + rewards.MinimumPurchaseAmount, min = rewards.MinimumPurchaseAmount, onetime = true, until = until, discount = rewards.CodeDiscountAmount, isfreeproduct = 1, affiliateName = reg.AffiliateName, require = rewards.RequiredProductIds, }; } db.UpsertRecord(p); var code = hashids.Encode(p.Id, reg.Id); p.Url = "/promotions/code/" + code; p.Title = "Affiliate Discount: " + code; if (rewards.IsFreeGiftInSaleOrder) { p.Title = "GIFT ITEM:" + rewards.Title; } db.UpsertRecord(p); claim = new AffiliateRewardsClaim(); claim.AffiliateRegistrationId = reg.Id; claim.NcbUserId = reg.NcbUserId; claim.AffiliateCode = reg.AffiliateCode; claim.DiscountCode = code; claim.RewardsName = rewards.Title; claim.AffiliateRewardsId = rewards.Id; claim.ProductId = p.Id; claim.CouponAttributes = p.Attributes; db.UpsertRecord(claim); }); return(claim); } { var claim = new AffiliateRewardsClaim(); claim.AffiliateRegistrationId = reg.Id; claim.NcbUserId = reg.NcbUserId; claim.AffiliateCode = reg.AffiliateCode; claim.RewardsName = rewards.Title; claim.AffiliateRewardsId = rewards.Id; claim.ProductId = rewards.RewardsProductId; db.UpsertRecord(claim); return(claim); } }
/// <summary> /// When Saleorder is set to waiting for order, generate InventoryItem for each item in the sale order /// TransformInventoryRequest event is called to finalize the list of items. /// </summary> /// <param name="db"></param> /// <param name="saleOrder"></param> internal static void ProcessSaleOrderUpdate(NancyBlackDatabase db, SaleOrder saleOrder, bool forceUpdate, DateTime now) { if (forceUpdate == false) { now = DateTime.Now; // fulfill the requests of this sale order // (No longer used) //InventoryItemModule.ProcessFulfillSaleOrder(db, saleOrder); // only do when status is waiting for order if (saleOrder.Status != SaleOrderStatus.WaitingForOrder) { return; } // if previous status is already waiting for order - do nothing if (db.GetOlderVersions(saleOrder).Any(s => s.Status == SaleOrderStatus.WaitingForOrder)) { return; } } if (saleOrder.Status == SaleOrderStatus.Cancel) { db.Transaction(() => { var relatedRequest = db.Query <InventoryItem>().Where(ivt => ivt.SaleOrderId == saleOrder.Id).ToList(); foreach (var item in relatedRequest) { // remove the usage from purchase var relatedPurchase = db.Query <InventoryPurchase>().Where(ip => ip.InventoryItemId == item.Id).ToList(); foreach (var p in relatedPurchase) { p.InventoryItemId = 0; db.UpsertRecord(p); } // remove the request from database db.DeleteRecord(item); } }); return; } if (saleOrder.PaymentStatus == PaymentStatus.PaymentReceived || saleOrder.PaymentStatus == PaymentStatus.Deposit || saleOrder.PaymentStatus == PaymentStatus.Credit) { } else { return; } var totalDiscount = 0M; var items = new List <InventoryItem>(); foreach (var item in saleOrder.ItemsDetail) { if (item.CurrentPrice < 0) // dont take negative prices (coupon) { totalDiscount += item.CurrentPrice * -1; // record the discount continue; } if (item.Url == "/dummy/dummy" && item.CurrentPrice == 0) { continue; } // For each items in sale order, create an inventory item for (int i = 0; i < (int)item.Attributes.Qty; i++) { // virtual products does not need inventory inbound if (item.Attributes.IsVirtual == 1) { continue; } var ivitm = new InventoryItem() { SaleOrderId = saleOrder.Id, ProductId = item.Id, RequestedDate = now, IsFullfilled = false, QuotedPrice = item.CurrentPrice, SellingPrice = item.CurrentPrice, }; items.Add(ivitm); if (item.CurrentPrice != item.Price) { totalDiscount += item.Price - item.CurrentPrice; } } } // distribute discount into items which has actual sell price var totalTrueItems = items.Where(item => item.QuotedPrice > 0).Count(); var discountRemaining = totalDiscount; while (discountRemaining > 0) { foreach (var item in items) { if (item.SellingPrice > 0) { // discount by 1% var discount = item.SellingPrice * 0.01M; if (discountRemaining - discount < 0) { discount = discountRemaining; } discountRemaining -= discount; item.SellingPrice = item.SellingPrice - discount; if (discountRemaining == 0) { break; } } } } var currentSite = AdminModule.ReadSiteSettings(); foreach (var item in items) { item.__updatedAt = DateTime.Now; item.__createdAt = DateTime.Now; if (item.SellingPrice > 0) { if (currentSite.commerce.billing.vattype == "addvat") { item.SellingTax = item.SellingPrice * (100 + (int)currentSite.commerce.billing.vatpercent) / 100; } if (currentSite.commerce.billing.vattype == "includevat") { var priceWithoutTax = item.SellingPrice * 100 / (100 + (int)currentSite.commerce.billing.vatpercent); item.SellingTax = item.SellingPrice - priceWithoutTax; item.SellingPrice = priceWithoutTax; } } } InventoryItemModule.TransformInventoryRequest(db, saleOrder, items); // NOTE: sometimes we give free, so price can be 0 items = (from item in items where item.SellingPrice >= 0 select item).ToList(); // attempt to merge the old list and new list using product id // by seeing if there is any item that was already fulfilled - if // there is any - copy the information into new list var existing = db.Query <InventoryItem>().Where(ivt => ivt.SaleOrderId == saleOrder.Id).ToLookup(ivt => ivt.ProductId); var newList = items.ToLookup(ivt => ivt.ProductId); foreach (var group in existing) { var existingGroup = existing[group.Key].ToList(); var newGroup = newList[group.Key].ToList(); for (int i = 0; i < existingGroup.Count; i++) { if (i >= newGroup.Count) { // old list has more items - keep them if it is already fulfilled if (existingGroup[i].IsFullfilled) { existingGroup[i].Note = "This sale order have less items, this is an orphaned row"; db.Connection.Update(existingGroup[i]); } else { // otherwise, just deletes them db.Connection.Delete <InventoryItem>(existingGroup[i].Id); } continue; } if (existingGroup[i].IsFullfilled) { // copy information from the existing one // for some reason not yet understand - i cant just remove newGroup[i] from items // and add existingGroup[i] into it instead! newGroup[i].IsFullfilled = true; newGroup[i].SerialNumber = existingGroup[i].SerialNumber; newGroup[i].FulfilledDate = existingGroup[i].FulfilledDate; newGroup[i].BuyingCost = existingGroup[i].BuyingCost; newGroup[i].BuyingTax = existingGroup[i].BuyingTax; newGroup[i].InventoryPurchaseId = existingGroup[i].InventoryPurchaseId; newGroup[i].Note = existingGroup[i].Note; } db.Connection.Delete <InventoryItem>(existingGroup[i].Id); } } db.Connection.InsertAll(items); }
internal static void ProcessReceiptCreation(NancyBlackDatabase db, Receipt obj) { // When payment receipt is created, create accounting entry db.Transaction(() => { var saleorder = db.GetById <SaleOrder>(obj.SaleOrderId); var paymentlog = db.GetById <PaymentLog>(obj.PaymentLogId); if (paymentlog.Amount <= 0) { return; // perhaps it is an error } if (saleorder == null || paymentlog == null) { // bogus receipt throw new InvalidOperationException("Invalid Receipt was created"); } var currentSite = saleorder.SiteSettings; if (currentSite == null) { currentSite = AdminModule.ReadSiteSettings(); } // Ensures all sale order logic has been ran // if the sale order was created before new system change if (saleorder.__createdAt < TaxSystemEpoch) { saleorder.UpdateSaleOrder(currentSite, db, false); } // Receipt will create 4 entries // 1) PaymentSource account increases, with amount paid // TODO: Mapping from PaymentSource to Account AccountingEntry entry1 = new AccountingEntry(); entry1.TransactionDate = paymentlog.__createdAt; entry1.TransactionType = "income"; entry1.DebtorLoanerName = "Customer"; entry1.IncreaseAccount = paymentlog.PaymentSource; entry1.IncreaseAmount = paymentlog.Amount; entry1.SaleOrderId = saleorder.Id; db.UpsertRecord(entry1); // 2) Sales Tax Calculation { AccountingEntry taxExtry = new AccountingEntry(); taxExtry.TransactionDate = paymentlog.__createdAt; taxExtry.TransactionType = "taxcredit"; taxExtry.DebtorLoanerName = "Tax"; taxExtry.DecreaseAccount = "Tax Credit"; taxExtry.SaleOrderId = saleorder.Id; if (currentSite.commerce.billing.vattype == "addvat") { var tax = paymentlog.Amount * ((100 + (Decimal)currentSite.commerce.billing.vatpercent) / 100); taxExtry.DecreaseAmount = tax * -1; } if (currentSite.commerce.billing.vattype == "includevat") { var tax = paymentlog.Amount * ((Decimal)currentSite.commerce.billing.vatpercent / 100); taxExtry.DecreaseAmount = tax * -1; } db.UpsertRecord(taxExtry); } // 3) Payment Fee if (paymentlog.Fee > 0) { AccountingEntry feeEntry = new AccountingEntry(); feeEntry.TransactionDate = paymentlog.__createdAt; feeEntry.TransactionType = "buy"; feeEntry.DebtorLoanerName = paymentlog.PaymentSource; feeEntry.IncreaseAccount = "Payment Fee - " + paymentlog.PaymentSource; feeEntry.IncreaseAmount = paymentlog.Fee; feeEntry.SaleOrderId = saleorder.Id; db.UpsertRecord(feeEntry); } // 4) Receivable from the Sale Order { // existing receivable of this sale order var existingReceivable = db.Query <AccountingEntry>().Where(e => e.SaleOrderId == saleorder.Id && e.IncreaseAccount == "Receivable").ToList(); // see if we have any receivable of this sale order // if we had, we have to deduct it if (existingReceivable.Count > 0) { AccountingEntry deductReceivableEntry = new AccountingEntry(); deductReceivableEntry.TransactionDate = paymentlog.__createdAt; deductReceivableEntry.TransactionType = "arpayment"; deductReceivableEntry.DebtorLoanerName = "Receivable From Sales"; deductReceivableEntry.DecreaseAccount = "Receivable"; deductReceivableEntry.DecreaseAmount = paymentlog.Amount; deductReceivableEntry.SaleOrderId = saleorder.Id; db.UpsertRecord(deductReceivableEntry); } else { // this maybe the first payment, see if all amount has been paid // see all payment log of this sale order // we only query payments up to currently processing payment log // so that when we re var payments = db.Query <PaymentLog>().Where(l => l.SaleOrderId == saleorder.Id && l.Id <= paymentlog.Id).ToList(); var remaining = saleorder.TotalAmount - payments.Sum(p => p.Amount); if (remaining > 0) { // this is partial payment - create new receivable AccountingEntry receivableEntry = new AccountingEntry(); receivableEntry.TransactionDate = paymentlog.__createdAt; receivableEntry.TransactionType = "newaccount"; receivableEntry.DebtorLoanerName = "Receivable From Sales"; receivableEntry.IncreaseAccount = "Receivable"; receivableEntry.IncreaseAmount = remaining; receivableEntry.SaleOrderId = saleorder.Id; db.UpsertRecord(receivableEntry); } // this is full payment in one go, no need for receivable } } }); }
public static void HandlePayment(NancyBlackDatabase db, PaymentLog log, DateTime paidWhen) { // ensure only one thread is processing this so lock (BaseModule.GetLockObject(log.SaleOrderIdentifier)) { db.Transaction(() => { paidWhen = DateTime.SpecifyKind(paidWhen, DateTimeKind.Utc); // find the sale order var so = db.Query <SaleOrder>() .Where(row => row.SaleOrderIdentifier == log.SaleOrderIdentifier) .FirstOrDefault(); bool isPaymentReceived = false; JArray exceptions = new JArray(); if (so == null) { exceptions.Add(JObject.FromObject(new { type = "Wrong SO Number", description = "Wrong SO Number" })); goto EndPayment; } log.SaleOrderId = so.Id; log.PaymentDate = paidWhen; // Wrong Payment Status if (so.PaymentStatus == PaymentStatus.PaymentReceived) { so.IsDuplicatePayment = true; exceptions.Add(JObject.FromObject(new { type = "Wrong Status", description = string.Format( "Current paymentlog status of SO is: {0}", PaymentStatus.DuplicatePayment) })); } // Error code received if (log.IsErrorCode) { so.PaymentStatus = PaymentStatus.WaitingForPayment; exceptions.Add(JObject.FromObject(new { type = "Error Code", description = "Error Code Received from Payment Processor: " + log.ResponseCode })); goto EndPayment; } var saleOrderPaymentLogs = db.Query <PaymentLog>() .Where(p => p.SaleOrderIdentifier == so.SaleOrderIdentifier); var totalSuccessful = saleOrderPaymentLogs.Where(l => l.IsPaymentSuccess).Count(); log.SuccessfulPaymentIndex = (totalSuccessful + 1) - 1; // after this line will never be run until EndPayment when IsErrorCode == true if (so.PaymentStatus != PaymentStatus.PaymentReceived && log.Amount != so.TotalAmount) { log.IsPaymentSuccess = true; so.PaymentStatus = PaymentStatus.Deposit; so.PaymentReceivedDate = DateTime.Now; // Need to use this to manage queue exceptions.Add(JObject.FromObject(new { type = "Split Payment", description = string.Format( "Expects: {0} amount from SO, payment is {1}", so.TotalAmount, log.Amount) })); var splitPaymentLogs = (from sPLog in saleOrderPaymentLogs where sPLog.IsErrorCode == false select sPLog).ToList(); isPaymentReceived = so.TotalAmount <= splitPaymentLogs.Sum(splog => splog.Amount) + log.Amount; } if (exceptions.Count == 0 || isPaymentReceived) { log.IsPaymentSuccess = true; so.PaymentStatus = PaymentStatus.PaymentReceived; so.PaymentReceivedDate = DateTime.Now; } EndPayment: log.Exception = exceptions; db.UpsertRecord <PaymentLog>(log); CommerceModule.PaymentOccured(so, db); if (log.IsPaymentSuccess) { // Set Receipt number var rc = db.UpsertRecord <Receipt>(new Receipt() { SaleOrderId = so.Id, PaymentLogId = log.Id }); db.UpsertRecord(rc); CommerceModule.PaymentSuccess(so, db); } db.UpsertRecord <SaleOrder>(so); // reset the one time code used foreach (var item in so.ItemsDetail) { if (item.Url.StartsWith("/promotions/code/archive-onetime")) { // do nothing } else if (item.Url.StartsWith("/promotions/code")) { if (item.Attributes.onetime != null) { var product = db.GetById <Product>(item.Id); if (product != null) { product.Url = product.Url.Replace("/promotions/code", "/promotions/code/archive-onetime"); db.UpsertRecord(product); } } } } // Automate change status to WaitingForOrder for add item to PO if (exceptions.Count == 0 || isPaymentReceived) { if (so.Status == SaleOrderStatus.Confirmed) { so.Status = SaleOrderStatus.WaitingForOrder; db.UpsertRecord <SaleOrder>(so); } } }); CommerceModule.SetReceiptIdentifier(db, paidWhen); } }
/// <summary> /// /// </summary> /// <param name="db"></param> public void UpdateSaleOrder(dynamic currentSite, NancyBlackDatabase db, bool save = true) { // Update Total this.TotalAmount = 0; // Total without discount decimal totalWithoutDiscount = 0; // snapshot the products into this sale order // so that if there is a change in product info later // we still have the one that customer sees var oldItemsDetail = this.ItemsDetail; this.ItemsDetail = new List <Product>(); //lookupItemDetail is used for provent duplication var lookupItemDetail = new Dictionary <int, Product>(); foreach (var item in this.Items) { var product = db.GetById <Product>(item); // in case some product no longer exist if (product == null) { var previousProduct = oldItemsDetail.Where(p => p.Id == item).FirstOrDefault(); this.ItemsDetail.Add(previousProduct); continue; } product.ContentParts = null; // check for duplication if (lookupItemDetail.ContainsKey(product.Id)) { var existProduct = lookupItemDetail[product.Id]; JObject attr = existProduct.Attributes; attr["Qty"] = attr.Value <int>("Qty") + 1; } else { JObject attr = product.Attributes; if (attr == null) { attr = new JObject(); product.Attributes = attr; } attr["Qty"] = 1; this.ItemsDetail.Add(product); lookupItemDetail.Add(product.Id, product); } this.TotalAmount += product.CurrentPrice; totalWithoutDiscount += product.Price; } // insert discount when there are some item with discount price (all in one discount) if (this.TotalAmount != totalWithoutDiscount) { // find all negative prices var totalNegativePrices = this.ItemsDetail.Where(i => i.CurrentPrice < 0).Sum(i => i.CurrentPrice); var attr = new JObject(); attr.Add("Qty", 1); var discount = new Product() { Title = "Discount", Price = this.TotalAmount - totalWithoutDiscount, Url = "/dummy/dummy", Attributes = attr }; this.ItemsDetail.Add(discount); this.TotalDiscount = (discount.Price + totalNegativePrices) * -1; this.TotalWithoutDiscount = totalWithoutDiscount + (totalNegativePrices * -1); } this.SetAllFee(currentSite); this.TotalAmount += this.ShippingFee + this.ShippingInsuranceFee + this.PaymentFee; this.TotalAmount = Math.Round(this.TotalAmount, 2, MidpointRounding.AwayFromZero); // TAX Calculation if (currentSite.commerce.billing.vattype == "addvat") { this.TotalAmountWithoutTax = this.TotalAmount; this.TotalTax = this.TotalAmountWithoutTax * (100 + (int)currentSite.commerce.billing.vatpercent) / 100; this.TotalAmount = this.TotalAmountWithoutTax + this.TotalTax; } if (currentSite.commerce.billing.vattype == "includevat") { this.TotalAmountWithoutTax = this.TotalAmount * 100 / (100 + (int)currentSite.commerce.billing.vatpercent); this.TotalTax = this.TotalAmount - this.TotalAmountWithoutTax; } if (!string.IsNullOrEmpty(this.Currency)) { JObject rate = CommerceAdminModule.ExchangeRate; decimal want = (decimal)rate.Property(this.Currency).Value; decimal home = (decimal)rate.Property("THB").Value; this.CurrencyConversionRate = want / home; Func <decimal, decimal> toWant = (decimal input) => input * this.CurrencyConversionRate * 1.03m; foreach (Product current in this.ItemsDetail) { current.Price = toWant(current.Price); current.DiscountPrice = toWant(current.DiscountPrice); } this.ShippingFee = toWant(this.ShippingFee); this.ShippingInsuranceFee = toWant(this.ShippingInsuranceFee); this.PaymentFee = toWant(this.PaymentFee); this.TotalAmount = toWant(this.TotalAmount); } this.SiteSettings = currentSite; if (save == false) { return; // Just update the details for calculation } db.Transaction(() => { // need to insert to get ID db.UpsertRecord <SaleOrder>(this); this.SaleOrderIdentifier = string.Format(CultureInfo.InvariantCulture, "SO{0:yyyyMMdd}-{1:000000}", this.__createdAt, this.Id); // save the SO ID again db.UpsertRecord <SaleOrder>(this); }); }
/// <summary> /// Updates the sale order /// </summary> /// <param name="db"></param> public void UpdateSaleOrder(dynamic currentSite, NancyBlackDatabase db, bool save = true) { // if we dont have site settings, use the one provided // otherwise use the remembered one if (this.SiteSettings == null) { this.SiteSettings = currentSite; } else { currentSite = this.SiteSettings; } // Update Total this.TotalAmount = 0; // Total without discount decimal totalWithoutDiscount = 0; // New Logic 2018 - we will primariry use itemsdetail // if it already exists - so that admin can add/remove items // freely and customer still sees the old prices //lookupItemDetail is used for prevent duplication var lookupItemDetail = new Dictionary <int, Product>(); Action <int> addnewProduct = (item) => { var product = db.GetById <Product>(item); if (product == null) { return; // id does not exists } product.ContentParts = null; product.MetaDescription = null; product.MetaKeywords = null; product.Layout = null; product.PromotionReferenceDate = DateTime.Today; // check for duplication if (lookupItemDetail.ContainsKey(product.Id)) { var existProduct = lookupItemDetail[product.Id]; JObject attr = existProduct.Attributes; attr["Qty"] = attr.Value <int>("Qty") + 1; } else { JObject attr = product.Attributes; if (attr == null) { attr = new JObject(); product.Attributes = attr; } attr["Qty"] = 1; this.ItemsDetail.Add(product); lookupItemDetail.Add(product.Id, product); } this.TotalAmount += product.CurrentPrice; if (product.Price > 0) { totalWithoutDiscount += product.Price; } }; // generate itemsdetail list from items list if not exists if (this.ItemsDetail == null || this.ItemsDetail.Count == 0) { this.ItemsDetail = new List <Product>(); foreach (var item in this.Items) { addnewProduct(item); } } else { HashSet <int> processedProductId = new HashSet <int>(); // otherwise - the items list is being generated from the itemsdetail list var newItemsList = new List <int>(); foreach (var item in this.ItemsDetail) { processedProductId.Add(item.Id); if (item.Url == "/dummy/dummy") { continue; } if (item.Attributes["Qty"] == null) { continue; } else { for (int i = 0; i < (int)item.Attributes["Qty"]; i++) { // suppose to throw because we cannot verify product price due to cannot specify PromotionReferenceDate if (item.PromotionReferenceDate == default(DateTime)) { item.PromotionReferenceDate = this.__createdAt; } newItemsList.Add(item.Id); this.TotalAmount += item.CurrentPrice; if (item.CurrentPrice > item.Price) // this is not discount { totalWithoutDiscount += item.CurrentPrice; } else if (item.CurrentPrice < 0 && item.CurrentPrice == item.Price) { // do nothing } else { totalWithoutDiscount += item.Price; } } } } foreach (var id in this.Items) { if (processedProductId.Contains(id) == false) { addnewProduct(id); newItemsList.Add(id); } } this.Items = newItemsList.ToArray(); } Func <Product, int> getSortOrder = (p) => { var orderList = new string[] { "/laptops/", "/monitor/", "/calibrate/", "/cpu/", "/gpu/", "/ram/", "/m2/", "/hdd/", "/wifi/", "/keyboard/", "/thermal/", "/os/" }; if (p.Url.Contains("/promotion")) { return(int.MaxValue); } for (int i = 0; i < orderList.Length; i++) { if (p.Url.IndexOf(orderList[i]) > 0) { return(i); } } return(int.MaxValue); }; this.ItemsDetail = this.ItemsDetail.OrderBy(p => getSortOrder(p)).ToList(); // remove the discount item var discountItem = this.ItemsDetail.Where(i => i.Url == "/dummy/dummy").FirstOrDefault(); if (discountItem != null) { this.ItemsDetail.Remove(discountItem); } // insert discount when there are some item with discount price (all in one discount) if (this.TotalAmount != totalWithoutDiscount) { // find all negative prices var totalNegativePrices = this.ItemsDetail.Where(i => i.CurrentPrice < 0).Sum(i => i.CurrentPrice); var attr = new JObject(); attr.Add("Qty", 1); if (this.TotalAmount > totalWithoutDiscount) { // discount is more than older amount // this can happen with item that has price = 0 discountItem = new Product() { Title = "Discount", Price = 0, Url = "/dummy/dummy", Attributes = attr }; } else { discountItem = new Product() { Title = "Discount", Price = this.TotalAmount - (totalWithoutDiscount + totalNegativePrices), Url = "/dummy/dummy", Attributes = attr }; } this.ItemsDetail.Add(discountItem); this.TotalDiscount = (discountItem.Price); this.TotalWithoutDiscount = totalWithoutDiscount; } else { this.TotalDiscount = 0; this.TotalWithoutDiscount = this.TotalAmount; } this.SetAllFee(); this.TotalAmount += this.ShippingFee + this.ShippingInsuranceFee + this.PaymentFee; this.TotalAmount = Math.Round(this.TotalAmount, 2, MidpointRounding.AwayFromZero); // TAX Calculation if (currentSite.commerce.billing.vattype == "addvat") { this.TotalAmountWithoutTax = this.TotalAmount; this.TotalTax = this.TotalAmountWithoutTax * (100 + (int)currentSite.commerce.billing.vatpercent) / 100; this.TotalAmount = this.TotalAmountWithoutTax + this.TotalTax; } if (currentSite.commerce.billing.vattype == "includevat") { this.TotalAmountWithoutTax = this.TotalAmount * 100 / (100 + (int)currentSite.commerce.billing.vatpercent); this.TotalTax = this.TotalAmount - this.TotalAmountWithoutTax; } if (save == false) { return; // Just update the details for calculation } db.Transaction(() => { // need to insert to get ID db.UpsertRecord <SaleOrder>(this); this.SaleOrderIdentifier = string.Format(CultureInfo.InvariantCulture, "SO{0:yyyyMMdd}-{1:000000}", this.__createdAt, this.Id); // save the SO ID again db.UpsertRecord <SaleOrder>(this); }); }
/// <summary> /// When Saleorder is set to waiting for order, generate InventoryItem for each item in the sale order /// TransformInventoryRequest event is called to finalize the list of items. /// </summary> /// <param name="db"></param> /// <param name="saleOrder"></param> internal static void ProcessSaleOrderUpdate(NancyBlackDatabase db, SaleOrder saleOrder, bool replay, DateTime now) { if (replay == false) { now = DateTime.Now; // only do when status is waiting for order if (saleOrder.Status != SaleOrderStatus.WaitingForOrder) { return; } // if previous status is already waiting for order - do nothing if (db.GetOlderVersions(saleOrder).First().Status == SaleOrderStatus.WaitingForOrder) { return; } } var currentSite = AdminModule.ReadSiteSettings(); // NOTE: We can't run it again since it can alter the amount // it is possible that admin may change amount in database //// ensures that all logic of sale order has been ran //saleOrder.UpdateSaleOrder(currentSite, db, false); // ensure that no inventory inbound can be run var totalDiscount = 0M; var items = new List <InventoryItem>(); foreach (var item in saleOrder.ItemsDetail) { if (item.CurrentPrice < 0) // dont take negative prices (coupon) { totalDiscount += item.CurrentPrice * -1; // record the discount continue; } // For each items in sale order, create an inventory item for (int i = 0; i < (int)item.Attributes.Qty; i++) { var ivitm = new InventoryItem() { SaleOrderId = saleOrder.Id, ProductId = item.Id, RequestedDate = now, IsFullfilled = false, SellingPrice = item.CurrentPrice }; items.Add(ivitm); if (item.CurrentPrice != item.Price) { totalDiscount += item.Price - item.CurrentPrice; } } } // distribute discount into items which has actual sell price var discountToDistribute = totalDiscount / items.Where(item => item.SellingPrice > 0).Count(); // discount is too great for some item, add it to the most expensive one if (items.Where(item => discountToDistribute > item.SellingPrice).Count() > 0) { var item = items.OrderByDescending(i => i.SellingPrice).First(); item.SellingPrice -= totalDiscount; if (currentSite.commerce.billing.vattype == "addvat") { item.SellingTax = item.SellingPrice * (100 + (int)currentSite.commerce.billing.vatpercent) / 100; } if (currentSite.commerce.billing.vattype == "includevat") { var priceWithoutTax = item.SellingPrice * 100 / (100 + (int)currentSite.commerce.billing.vatpercent); item.SellingTax = item.SellingPrice - priceWithoutTax; item.SellingPrice = priceWithoutTax; } } else // distribute it to items { foreach (var item in items) { if (item.SellingPrice > 0) { item.SellingPrice -= discountToDistribute; if (currentSite.commerce.billing.vattype == "addvat") { item.SellingTax = item.SellingPrice * (100 + (int)currentSite.commerce.billing.vatpercent) / 100; } if (currentSite.commerce.billing.vattype == "includevat") { var priceWithoutTax = item.SellingPrice * 100 / (100 + (int)currentSite.commerce.billing.vatpercent); item.SellingTax = item.SellingPrice - priceWithoutTax; item.SellingPrice = priceWithoutTax; } } } } InventoryAdminModule.TransformInventoryRequest(db, saleOrder, items); db.Transaction(() => { // before inserting... // if the inventory item for this sale order already fullfilled // it will remain in inventory but sale order removed // we will always create new inventory item for this sale order // and clear out old ones foreach (var item in db.Query <InventoryItem>().Where(ivt => ivt.SaleOrderId == saleOrder.Id).ToList()) { if (item.IsFullfilled) { item.Note = "Sale Order Id was removed because sale order which created this item has status set to WaitingForOrder Again"; item.SaleOrderId = 0; item.IsFullfilled = false; db.UpsertRecord(item); continue; // item already fullfilled, we leave it but remove sale order id } db.DeleteRecord(item); } foreach (var item in items) { db.UpsertRecord(item); } }); }