/// <summary> /// Check:: Booking Request expiration /// If::Provider didn't reply /// If::Request not updated/changed /// Action:: Set as expired, un-authorize/return money to customer, notify /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingRequestExpiration(LcLogger logger, Database db) { var messages = 0L; var items = 0L; foreach (var b in LcRest.Booking.QueryRequest2ExpiredBookings(db)) { try { // RequestStatusID:6:expired b.ExpireBooking(); // Send message LcMessaging.SendBooking.For(b.bookingID).BookingRequestExpired(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Booking Request Expiration]"); items++; messages += 2; } catch (Exception ex) { logger.LogEx("Booking Request Expired", ex); } } logger.Log("Total invalidated as Expired Booking Requests: {0}, messages sent: {1}", items, messages); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
/// <summary> /// Saves on database the updated information for a payment account with the notified information that /// means an change on the payment gateway for that object. /// Braintree sends notifications through Webhooks to a configured URL, our page at that address /// manage it and call this when matched the Kind of notification related to the creation request /// for a Sub-merchant or Merchant account (aka provider payment account). /// </summary> /// <param name="notification"></param> public static void RegisterProviderPaymentAccountCreationNotification(WebhookNotification notification, string signature, string payload) { // If is not a SubMerchant creation, skip (maybe a new main merchant account was created) if (!notification.MerchantAccount.IsSubMerchant) { return; } var providerID = LcUtils.ExtractInt(notification.MerchantAccount.Id, 0); // Is not valid user if (providerID == 0) { using (var logger = new LcLogger("PaymentGatewayWebhook")) { logger.Log("SubMerchantAccount:: Impossible to get the provider UserID from next MerchantAccountID: {0}", notification.MerchantAccount.Id); logger.Log("SubMerchantAccount:: Follows signature and payload"); logger.LogData(signature); logger.LogData(payload); logger.Save(); } return; } LcData.SetProviderPaymentAccount( providerID, notification.MerchantAccount.Id, notification.MerchantAccount.Status.ToString(), notification.Message, signature, payload ); }
/// <summary> /// Check the due date of partnership plan subscriptions, setting them as expired is out of date. /// </summary> /// <param name="list"></param> /// <param name="logger"></param> private void EnforceDueDate(IEnumerable <LcRest.UserPaymentPlan> list, LcLogger logger) { long reviewed = 0; long processed = 0; foreach (var userPlan in list) { try { // CCCPlan subscriptions if (userPlan.paymentPlan == LcEnum.SubscriptionPlan.CccPlan && userPlan.nextPaymentDueDate.HasValue) { reviewed++; if (userPlan.nextPaymentDueDate.Value < DateTimeOffset.Now) { // Set as ended userPlan.subscriptionEndDate = DateTimeOffset.Now; // Set as expired, same value as Gateway/Braintree userPlan.planStatus = Braintree.SubscriptionStatus.EXPIRED.ToString(); // Update DB LcRest.UserPaymentPlan.Set(userPlan); processed++; } } } catch (Exception ex) { logger.LogEx("Check partnership subscriptions date for Past Due", ex); } } logger.Log("Partnership subscriptions updated because of Past Due: {0} from total reviewed {1}", processed, reviewed); ItemsReviewed += reviewed; ItemsProcessed += processed; }
public static void LogAspnetError(Exception ex) { var Request = HttpContext.Current.Request; using (var logger = new LcLogger("aspnet-errors")) { try { logger.Log("Page error, unhandled exception caugth at Global.asax, context:"); if (WebMatrix.WebData.WebSecurity.IsAuthenticated) { logger.Log("User:: {0}:{1}", WebMatrix.WebData.WebSecurity.CurrentUserId, WebMatrix.WebData.WebSecurity.CurrentUserName); } else { logger.Log("User:: Anonymous"); } logger.Log("Request:: {0} {1}", Request.HttpMethod, Request.RawUrl); logger.Log("User-Agent:: {0}", Request.UserAgent); try { logger.Log("Form Data::"); logger.LogData(ASP.LcHelpers.NameValueToString(Request.Form)); } catch { } logger.LogEx("Page error details", ex); } catch { } logger.Save(); } }
public static void Set(UserPaymentPlan data, LcDatabase sharedDb = null) { using (var db = new LcDatabase(sharedDb)) { db.Execute(sqlSet, data.userPaymentPlanID, data.userID, data.subscriptionID, data.paymentPlan.ToString(), data.paymentMethod, data.paymentPlanLastChangedDate, data.nextPaymentDueDate, data.nextPaymentAmount, data.firstBillingDate, data.subscriptionEndDate, data.paymentMethodToken, data.paymentExpiryDate, data.planStatus, data.daysPastDue ); try { // Set OwnerStatus if (IsPartnershipPlan(data.paymentPlan)) { var ow = new Owner(); ow.userID = data.userID; ow.statusID = (int)OwnerStatus.notYetAnOwner; Owner.Set(ow); } else { // Run Membership Checks to enable/disable member (OwnerStatus update) UserProfile.CheckAndSaveOwnerStatus(data.userID); } } catch (Exception ex) { // An error checking status must NOT prevent us from saving/creating // the payment-plan, but we must notify staff so we can take manual action // to fix the error and run the check again for this user try { LcLogger.LogAspnetError(ex); LcMessaging.NotifyError("UserPaymentPlan.Set->UserProfile.CheckAndSaveOwnerStatus::userID=" + data.userID, System.Web.HttpContext.Current.Request.RawUrl, ex.ToString()); } catch { // Prevent cancel paymentplan creation/update because of email or log failing. Really strange // and webhook-scheduleTask for subscriptions would attempt again this. } } } }
/// <summary> /// Check:: Release Payment for Service Complete: 1 hour 15 min after service is performed /// (before #844 was 1 day after the service is performed) /// If:: Provider has already completed bookings (is not a new provider) /// If:: Performed bookings only, without pricing adjustment /// If:: Current time is 1 hour 15 min after Confirmed Service EndTime (before #844 was 1 day) /// Action:: set booking status as 'completed', /// send a messages. /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingReleasePayment(LcLogger logger, Database db) { var messages = 0L; var items = 0L; // NOTE: Changed to ALL providers at 2016-01-26 as of #844 foreach (var b in LcRest.Booking.QueryPendingOfPaymentReleaseBookings(null, db)) { try { // Release the payment try { if (b.ReleasePayment()) { items++; // Send messages // Notify customer and provider with an updated booking details: LcMessaging.SendBooking.For(b.bookingID).BookingCompleted(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Release Payment 1H]"); messages += 2; } } catch (Exception ex) { var errTitle = "Booking Release Payment after 1H to providers"; var errDesc = String.Format( "BookingID: {0}, TransactionID: {1}. Not payed on [Release Payment 1H], error on Braintree 'release transaction from escrow': {2}", b.bookingID, b.paymentTransactionID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); } } catch (Exception ex) { logger.LogEx("Booking Release Payment 1H", ex); } } logger.Log("Total of Booking Release Payment after 1H: {0}, messages sent: {1}", items, messages); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
/// <summary> /// Check:: Setting No-Payment Bookings as Complete: 1 hour 15 min after service is performed /// If::Performed bookings only /// If:: Current time is 1 hour 15 min after Confirmed Service EndTime /// Action:: set booking status as 'completed', /// send messages. /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingNoPaymentComplete(LcLogger logger, Database db) { var messages = 0L; var items = 0L; foreach (var b in LcRest.Booking.QueryPendingOfCompleteWithoutPaymentBookings(db)) { try { // Release the payment try { b.SetBookingAsCompleted(); items++; // Send messages // Notify customer and provider with an updated booking details: LcMessaging.SendBooking.For(b.bookingID).BookingCompleted(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Complete - no payment]"); messages += 2; } catch (Exception ex) { var errTitle = "Setting No-Payment Bookings as Complete after 1H15M to providers"; var errDesc = String.Format( "BookingID: {0}, Error: {1}", b.bookingID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); } } catch (Exception ex) { logger.LogEx("Setting No-Payment Bookings as Complete 1H15M", ex); } } logger.Log("Total of No-Payment Bookings set as Complete after 1H15M: {0}, messages sent: {1}", items, messages); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
private void UpdateFromGateway(IEnumerable <LcRest.UserPaymentPlan> list, LcLogger logger) { long reviewed = 0; long processed = 0; foreach (var userPlan in list) { try { // Query Braintree subscription var m = new LcPayment.Membership(); var subs = m.GetSubscription(userPlan.subscriptionID); reviewed++; if (subs == null) { // Problem: saved ID didn't found at Braintree, corrupted data var err = String.Format("Database subscriptionID '{0}' not found at Braintree, corrupted data." + " Please review user {1} subscription", userPlan.subscriptionID, userPlan.userID); LcMessaging.NotifyError("UserPaymentPlanSubscriptionUpdatesTask", "ScheduledTask", err); logger.Log(err); } else { // If status changed, update record if (userPlan.planStatus != subs.Status.ToString()) { userPlan.UpdatedAtGateway(subs); LcRest.UserPaymentPlan.Set(userPlan); // Update items count processed++; // Payments foreach (var transaction in subs.Transactions) { var payment = LcRest.UserFeePayment.FromSubscriptionTransaction(userPlan, transaction); LcRest.UserFeePayment.Set(payment); } } } } catch (Exception ex) { logger.LogEx("Pending and Past Due subscriptions", ex); } } logger.Log("Subscriptions updated from gateway: {0} from total reviewed {1}", processed, reviewed); ItemsReviewed += reviewed; ItemsProcessed += processed; }
/// <summary> /// Check:: Authorize postponed transactions 24hours previous to service start-time /// If::Confirmed or performed bookings only, not cancelled or in dispute or completed(completed may be /// and old booking already paid /// If::Current time is 24 hours before Confirmed Service StartTime /// If:: BookingRequest PaymentTransactionID is a Card token rather than an actual TransactionID /// If::Customer was still not charged / transaction was not submitted for settlement ([ClientPayment] is null) /// Action::authorize booking transaction /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingAuthorizePostponedTransactions(LcLogger logger, Database db) { var messages = 0L; var items = 0L; foreach (var b in LcRest.Booking.QueryPostponedPaymentAuthorizations(db)) { try { // Create transaction authorizing charge (but actually not charging still) // for saved customer credit card and update DB. try { if (b.AuthorizeTransaction()) { items++; } } catch (Exception ex) { var errTitle = "Booking Authorize Postponed Transactions, 24h before Service Starts"; var errDesc = String.Format( "BookingID: {0}, TransactionID: {1} Payment not allowed, error on Braintree 'sale transaction, authorizing only': {2}", b.bookingID, b.paymentTransactionID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); // DOUBT: Notify providers on failed authorization/receive-payment? } } catch (Exception ex) { logger.LogEx("Booking Authorize Postponed Transactions, 24h before Service Starts", ex); } } logger.Log("Total of Booking Authorize Postponed Transactions, 24h before Service Starts: {0}", items); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
public static EarningsEntryReminderTask Run(LcLogger logger) { var task = new EarningsEntryReminderTask(logger); using (var db = new LcDatabase()) { task.QueryUsers(db).All(task.ProcessUser); } task.TaskEnded = DateTime.Now; logger.Log("Elapsed time {0}, for {1} Earnings Entry Reminders sent, out of {2} records reviewed", task.ElapsedTime, task.ItemsProcessed, task.ItemsReviewed); return(task); }
public static UserPaymentPlanSubscriptionUpdatesTask Run(LcLogger logger) { var task = new UserPaymentPlanSubscriptionUpdatesTask(); using (var db = new LcDatabase()) { var list = LcRest.UserPaymentPlan.QueryActiveSubscriptions(db); task.UpdateFromGateway(list, logger); } task.TaskEnded = DateTime.Now; logger.Log("Elapsed time {0}, for {1} subscriptions processed, {2} records reviewed", task.ElapsedTime, task.ItemsProcessed, task.ItemsReviewed); return(task); }
/// <summary> /// Check:: Charge Customer the day of the service /// If::Confirmated or performed bookings only, not cancelled or in dispute or completed(completed may be /// and old booking already paid /// If::Current time is the 1 hour after the End Service, or later /// If::Customer was still not charged / transaction was not submitted for settlement ([TotalPricePaidByCustomer] is null) /// Action::settle booking transaction /// set[TotalPricePaidByCustomer] and[TotalServiceFeesPaidByCustomer] values /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingChargeCustomer(LcLogger logger, Database db) { var messages = 0L; var items = 0L; // Get bookings affected by conditions foreach (var b in LcRest.Booking.QueryPendingOfClientChargeBookings(db)) { try { // Charge customer and update DB try { if (b.SettleTransaction()) { items++; } } catch (Exception ex) { var errTitle = "Booking Charge Customer, Receive Payment"; var errDesc = String.Format( "BookingID: {0}, TransactionID: {1} Payment not received, error on Braintree 'settle transaction': {2}", b.bookingID, b.paymentTransactionID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); } } catch (Exception ex) { logger.LogEx("Booking Charge Customer, Receive Payment", ex); } } logger.Log("Total of Booking Charge Customer, Receive Payment: {0}", items); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
/// <summary> /// Check:: [48H Service Reminder] Booking will be on 48Hours /// If::Confirmated bookings not cancelled /// If::Current time is 48 hours before Confirmed Service StarTime /// Action:: send a booking reminder email /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBooking48HReminder(LcLogger logger, Database db) { var messages = 0L; var items = 0L; foreach (var b in db.Query(@" SELECT BookingID FROM Booking As B INNER JOIN CalendarEvents As E ON B.ServiceDateID = E.Id WHERE BookingStatusID = @0 AND -- at 48 hours before service starts (between 49 and 48 hours) getdate() > dateadd(hh, -49, E.StartTime) AND getdate() <= dateadd(hh, -48, E.StartTime) AND B.MessagingLog not like '%[48H Service Reminder]%' ", (int)LcEnum.BookingStatus.confirmed)) { try { // Send message LcMessaging.SendBooking.For(b.BookingID).BookingReminder(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.BookingID, "[48H Service Reminder]"); items++; messages += 2; } catch (Exception ex) { logger.LogEx("Booking 48H Reminders", ex); } } logger.Log("Total of Booking 48H Reminders: {0}, messages sent: {1}", items, messages); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
public static UserPaymentPlanSubscriptionUpdatesTask Run(LcLogger logger) { var task = new UserPaymentPlanSubscriptionUpdatesTask(); using (var db = new LcDatabase()) { // Subcriptions that need udpate from Gateway var list = LcRest.UserPaymentPlan.QueryActiveSubscriptions(true, db); task.UpdateFromGateway(list, logger); // No-payment subscriptions, currently just partnership plans, that need manual check of due date task.EnforceDueDate(LcRest.UserPaymentPlan.QueryActiveSubscriptions(false, db), logger); } task.TaskEnded = DateTime.Now; logger.Log("Elapsed time {0}, for {1} subscriptions processed, {2} records reviewed", task.ElapsedTime, task.ItemsProcessed, task.ItemsReviewed); return(task); }
private static void RunIcalendar(LcLogger logger) { DateTime partialElapsedTime = DateTime.Now; int successCalendars = 0, failedCalendars = 0; foreach (var err in LcCalendar.BulkImport()) { if (err == null) { successCalendars++; } else { failedCalendars++; logger.LogEx("Import Calendar", err); } } logger.Log("Elapsed time {0}, for {1} user calendars imported, {2} failed", DateTime.Now - partialElapsedTime, successCalendars, failedCalendars); }
/// <summary> /// Check:: Booking timed out /// If::A not complete booking request exist without changes from more than 1 day /// Action:: Invalidate the booking tentative events /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingTimedOut(LcLogger logger, Database db) { var items = 0L; foreach (var b in LcRest.Booking.QueryIncomplete2TimedoutBookings(db)) { try { LcRest.Booking.SetAsTimedout(b, db); items++; } catch (Exception ex) { logger.LogEx("Booking Timed-out", ex); } } logger.Log("Total invalidated as TimedOut Booking: {0}, messages sent: {1}", items, 0); return(new TaskResult { ItemsProcessed = items }); }
/// <summary> /// Check:: Service Performed: The end of the service (before #844, was at 48H passed from Service) /// If::Confirmated bookings only, not cancelled, not set as performed, complete or dispute /// If:: Current time is Confirmed Service EndTime /// Action:: set booking status as 'service-performed' /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingServicePerformed(LcLogger logger, Database db) { var messages = 0L; var items = 0L; foreach (var b in LcRest.Booking.QueryConfirmed2ServicePerformedBookings(db)) { try { // Set as servicePerformed b.bookingStatusID = (int)LcEnum.BookingStatus.servicePerformed; LcRest.Booking.SetStatus(b, db); // Send messages // Notify customer and provider with an updated booking details: LcMessaging.SendBooking.For(b.bookingID).ServicePerformed(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Service Performed]"); items++; // Before Marketplace: messages += 3; messages += 2; } catch (Exception ex) { logger.LogEx("Booking Service Performed", ex); } } logger.Log("Total of Booking Service Performed: {0}, messages sent: {1}", items, messages); return(new TaskResult { ItemsProcessed = items, MessagesSent = messages }); }
/// <summary> /// Performs a transaction to authorize the payment on the client payment method, but /// not charging still, using the data from the given booking and the saved paymentMethodID. /// Booking is NOT checked before perform the task, use the LcRest.Booking API to securely run pre-condition /// checks before authorize transaction. The booking must have the data loaded for the pricingSummary. /// /// REVIEWED #771 /// </summary> /// <param name="booking"></param> /// <param name="paymentMethodID">AKA creditCardToken</param> /// <returns>It returns the transactionID generated, original booking object is not updated. /// Errors in the process are throwed.</returns> public static string AuthorizeBookingTransaction(LcRest.Booking booking) { if (booking.pricingSummary == null || !booking.pricingSummary.totalPrice.HasValue || booking.pricingSummary.totalPrice.Value <= 0) { throw new ConstraintException("To authorize a booking payment is required a price to charge."); } var gateway = NewBraintreeGateway(); TransactionRequest request = new TransactionRequest { Amount = booking.pricingSummary.totalPrice.Value, // Marketplace #408: since provider receive the money directly, Braintree must discount // the next amount in concept of fees and pay that to the Marketplace Owner (us, Loconomics ;-) ServiceFeeAmount = booking.pricingSummary.serviceFeeAmount, CustomerId = GetCustomerId(booking.clientUserID), PaymentMethodToken = booking.paymentMethodID, // Now, with Marketplace #408, the receiver of the money for each transaction is // the provider through account at Braintree, and not the Loconomics account: //MerchantAccountId = LcPayment.BraintreeMerchantAccountId, MerchantAccountId = GetProviderPaymentAccountId(booking.serviceProfessionalUserID), Options = new TransactionOptionsRequest { // Marketplace #408: don't pay provider still, wait for the final confirmation 'release scrow' HoldInEscrow = true, // Do not submit, just authorize: SubmitForSettlement = false } }; var r = gateway.Transaction.Sale(request); // Everything goes fine if (r.IsSuccess()) { // Get the transactionID if (r.Target != null && !String.IsNullOrEmpty(r.Target.Id)) { // If the card is a TEMPorarly card (just to perform this transaction) // it must be removed now since was successful used // IMPORTANT: Since an error on this subtask is not important to the // user and will break a success process creating a new problem if throwed (because transactionID // gets lost), // is catched and managed internally by Loconomics stuff that can check and fix transparentely // this minor error. try { if (booking.paymentMethodID.StartsWith(TempSavedCardPrefix)) { gateway.CreditCard.Delete(booking.paymentMethodID); } } catch (Exception ex) { try { LcMessaging.NotifyError(String.Format("LcPayment.AuthorizeBookingTransaction..DeleteBraintreeTempCard({0});bookingID={1}", booking.paymentMethodID, booking.bookingID), "", ex.Message); LcLogger.LogAspnetError(ex); } catch { } } // r.Target.Id => transactionID return(r.Target.Id); } else { // Transaction worked but impossible to know the transactionID (weird, is even possible?), // notify error throw new Exception("Impossible to know transaction details, please contact support. BookingID #" + booking.bookingID.ToString()); } } else { throw new Exception(r.Message); } }
/// <summary> /// Runs all tasks if the conditions for each are met. /// </summary> /// <returns>Logged string</returns> public static string Run() { var logger = new LcLogger("ScheduledTask"); var elapsedTime = DateTime.Now; /* * Bookings */ var totalmessages = 0L; var totalitems = 0L; TaskResult result; var taskRunners = new List <BookingTaskRunner> { RunBookingTimedOut, RunBookingRequestExpiration, RunBooking48HReminder, RunBookingAuthorizePostponedTransactions, RunBookingChargeCustomer, RunBookingServicePerformed, RunBookingReleasePayment, RunBookingNoPaymentComplete, RunBookingReviewReminder1W }; using (var db = Database.Open("sqlloco")) { foreach (var runner in taskRunners) { try { result = runner(logger, db); totalitems += result.ItemsProcessed; totalmessages += result.MessagesSent; } catch (Exception ex) { logger.LogEx(runner.Method.Name, ex); } } // Ending work with database } logger.Log("Elapsed time {0}, for {1} bookings affected and {2} messages sent", DateTime.Now - elapsedTime, totalitems, totalmessages); /* * iCalendar */ try { RunIcalendar(logger); } catch (Exception err) { logger.LogEx("Import Calendar", err); } /* * Membership tasks */ // Note: the whole ScheduleTask is set-up to run every hour, // since the subscriptions status is keep up to date by Webhooks already, // and this task is just a fallback in case of communication error and can // perform lot of request to Braintree, we force to reduce execution to once // a week. // Easily, we just check if we are at Midnight of Monday if (DateTime.Now.Hour == 0 && DateTime.Now.DayOfWeek == DayOfWeek.Monday) { try { Tasks.UserPaymentPlanSubscriptionUpdatesTask.Run(logger); } catch (Exception err) { logger.LogEx("UserPaymentPlanSubscriptionUpdatesTask", err); } } // Earnings reminder are sent only two times a month, so we check the day and the hour // (the hour to make it just once at the matched day, since the task executes every hour) if ((DateTime.Now.Day == 1 || DateTime.Now.Day == 15) && DateTime.Now.Hour == 7) { try { Tasks.EarningsEntryReminderTask.Run(logger); } catch (Exception err) { logger.LogEx("EarningsEntryReminderTask", err); } } /* * Task Ended */ logger.Log("Total Elapsed time {0}", DateTime.Now - elapsedTime); string logresult = logger.ToString(); // Finishing: save log on disk, per month rotation //try { logger.Save(); // Send Email too if exceptions found if (logger.HasExceptions) { LcMessaging.NotifyError("ScheduleTask throw Exceptions", "/ScheduledTask", logresult); } //}catch { } return(logresult); }
/// <summary> /// Runs all tasks if the conditions for each are met. /// </summary> /// <returns>Logged string</returns> public static string Run() { var logger = new LcLogger("ScheduledTask"); var elapsedTime = DateTime.Now; /* * Bookings */ int messages = 0, items = 0; int totalmessages = 0, totalitems = 0; var sqlAddBookingMessagingLog = "UPDATE Booking SET MessagingLog = coalesce(MessagingLog, '') + @1 WHERE BookingID=@0"; using (var db = Database.Open("sqlloco")) { /* * Check:: Booking timed out * If:: A not complete booking request exist without changes from more than 1 day * Action:: Invalidate the booking tentative events */ messages = 0; items = 0; foreach (var b in LcRest.Booking.QueryIncomplete2TimedoutBookings(db)) { try { LcRest.Booking.SetAsTimedout(b, db); items++; } catch (Exception ex) { logger.LogEx("Booking Timed-out", ex); } } logger.Log("Total invalidated as TimedOut Booking: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; /* * Check:: Booking Request expiration * If:: Provider didn't reply * If:: Request not updated/changed * Action:: Set as expired, un-authorize/return money to customer, notify */ messages = 0; items = 0; foreach (var b in LcRest.Booking.QueryRequest2ExpiredBookings(db)) { try { // RequestStatusID:6:expired b.ExpireBooking(); // Send message LcMessaging.SendBooking.For(b.bookingID).BookingRequestExpired(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Booking Request Expiration]"); items++; messages += 2; } catch (Exception ex) { logger.LogEx("Booking Request Expired", ex); } } logger.Log("Total invalidated as Expired Booking Requests: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; /* * Check:: [48H Service Reminder] Booking will be on 48Hours * If:: Confirmated bookings not cancelled * If:: Current time is 48 hours before Confirmed Service StarTime * Action:: send a booking reminder email */ messages = 0; items = 0; foreach (var b in db.Query(@" SELECT BookingID FROM Booking As B INNER JOIN CalendarEvents As E ON B.ServiceDateID = E.Id WHERE BookingStatusID = @0 AND -- at 48 hours before service starts (between 49 and 48 hours) getdate() > dateadd(hh, -49, E.StartTime) AND getdate() <= dateadd(hh, -48, E.StartTime) AND B.MessagingLog not like '%[48H Service Reminder]%' ", (int)LcEnum.BookingStatus.confirmed)) { try { // Send message LcMessaging.SendBooking.For(b.BookingID).BookingReminder(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.BookingID, "[48H Service Reminder]"); items++; messages += 2; } catch (Exception ex) { logger.LogEx("Booking 48H Reminders", ex); } } logger.Log("Total of Booking 48H Reminders: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; /* * Check:: Authorize postponed transactions 24hours previous to service start-time * If:: Confirmed or performed bookings only, not cancelled or in dispute or completed (completed may be * and old booking already paid * If:: Current time is 24 hours before Confirmed Service StartTime * If:: BookingRequest PaymentTransactionID is a Card token rather than an actual TransactionID * If:: Customer was still not charged / transaction was not submitted for settlement ([ClientPayment] is null) * Action:: authorize booking transaction */ items = 0; { foreach (var b in LcRest.Booking.QueryPostponedPaymentAuthorizations(db)) { try { // Create transaction authorizing charge (but actually not charging still) // for saved customer credit card and update DB. try { if (b.AuthorizeTransaction()) { items++; } } catch (Exception ex) { var errTitle = "Booking Authorize Postponed Transactions, 24h before Service Starts"; var errDesc = String.Format( "BookingID: {0}, TransactionID: {1} Payment not allowed, error on Braintree 'sale transaction, authorizing only': {2}", b.bookingID, b.paymentTransactionID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); // DOUBT: Notify providers on failed authorization/receive-payment? } } catch (Exception ex) { logger.LogEx("Booking Authorize Postponed Transactions, 24h before Service Starts", ex); } } } logger.Log("Total of Booking Authorize Postponed Transactions, 24h before Service Starts: {0}", items); totalitems += items; /* * Check:: Charge Customer the day of the service * If:: Confirmated or performed bookings only, not cancelled or in dispute or completed (completed may be * and old booking already paid * If:: Current time is the 1 hour after the End Service, or later * If:: Customer was still not charged / transaction was not submitted for settlement ([TotalPricePaidByCustomer] is null) * Action:: settle booking transaction * set [TotalPricePaidByCustomer] and [TotalServiceFeesPaidByCustomer] values */ items = 0; { // Get bookings affected by conditions foreach (var b in LcRest.Booking.QueryPendingOfClientChargeBookings(db)) { try { // Charge customer and update DB try { if (b.SettleTransaction()) { items++; } } catch (Exception ex) { var errTitle = "Booking Charge Customer, Receive Payment"; var errDesc = String.Format( "BookingID: {0}, TransactionID: {1} Payment not received, error on Braintree 'settle transaction': {2}", b.bookingID, b.paymentTransactionID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); } } catch (Exception ex) { logger.LogEx("Booking Charge Customer, Receive Payment", ex); } } } logger.Log("Total of Booking Charge Customer, Receive Payment: {0}", items); totalitems += items; /* * Check:: Service Performed: The end of the service (before #844, was at 48H passed from Service) * If:: Confirmated bookings only, not cancelled, not set as performed, complete or dispute * If:: Current time is Confirmed Service EndTime * Action:: set booking status as 'service-performed' */ messages = 0; items = 0; { foreach (var b in LcRest.Booking.QueryConfirmed2ServicePerformedBookings(db)) { try { // Set as servicePerformed b.bookingStatusID = (int)LcEnum.BookingStatus.servicePerformed; LcRest.Booking.SetStatus(b, db); // Send messages // Notify customer and provider with an updated booking details: LcMessaging.SendBooking.For(b.bookingID).ServicePerformed(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Service Performed]"); items++; // Before Marketplace: messages += 3; messages += 2; } catch (Exception ex) { logger.LogEx("Booking Service Performed", ex); } } } logger.Log("Total of Booking Service Performed: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; /* * Check:: Release Payment for New Providers: 5 full days after the service is performed * If:: If provider is a new provider (it has not previous completed bookings) * If:: Performed bookings only, without pricing adjustment * If:: Current time is 5 days after Confirmed Service EndTime * Action:: set booking status as 'completed', * send a message to the provider notifying that payment is released. */ /* REMOVED AS OF #844, 2016-01-26 * messages = 0; * items = 0; * { * foreach (var b in LcRest.Booking.QueryPendingOfPaymentReleaseBookings(true, db)) * { * try * { * // Release the payment * try * { * if (b.ReleasePayment()) * { * items++; * * // Send messages * * // Notify customer and provider with an updated booking details: * LcMessaging.SendBooking.For(b.bookingID).BookingCompleted(); * * // Update MessagingLog for the booking * db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Release Payment 120H New Provider]"); * * messages += 2; * } * } * catch (Exception ex) * { * * var errTitle = "Booking Release Payment after 120H for new providers"; * var errDesc = String.Format( * "BookingID: {0}, TransactionID: {1}. Not payed on [Release Payment 120H New Provider], error on Braintree 'release transaction from escrow': {2}", * b.bookingID, * b.paymentTransactionID, * ex.Message * ); * * LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); * * logger.Log("Error on: " + errTitle + "; " + errDesc); * } * } * catch (Exception ex) * { * logger.LogEx("Booking Release Payment after 120H for new providers", ex); * } * } * } * logger.Log("Total of Booking Release Payment after 120H for new providers: {0}, messages sent: {1}", items, messages); * totalitems += items; * totalmessages += messages; */ /* * Check:: Release Payment for Service Complete: 1 hour 15 min after service is performed * (before #844 was 1 day after the service is performed) * //If:: Provider has already completed bookings (is not a new provider) * If:: Performed bookings only, without pricing adjustment * If:: Current time is 1 hour 15 min after Confirmed Service EndTime (before #844 was 1 day) * Action:: set booking status as 'completed', * send a messages. */ messages = 0; items = 0; { // NOTE: Changed to ALL providers at 2016-01-26 as of #844 foreach (var b in LcRest.Booking.QueryPendingOfPaymentReleaseBookings(null, db)) { try { // Release the payment try { if (b.ReleasePayment()) { items++; // Send messages // Notify customer and provider with an updated booking details: LcMessaging.SendBooking.For(b.bookingID).BookingCompleted(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Release Payment 1H]"); messages += 2; } } catch (Exception ex) { var errTitle = "Booking Release Payment after 1H to providers"; var errDesc = String.Format( "BookingID: {0}, TransactionID: {1}. Not payed on [Release Payment 1H], error on Braintree 'release transaction from escrow': {2}", b.bookingID, b.paymentTransactionID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); } } catch (Exception ex) { logger.LogEx("Booking Release Payment 1H", ex); } } } logger.Log("Total of Booking Release Payment after 1H: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; /* * Check:: Setting No-Payment Bookings as Complete: 1 hour 15 min after service is performed * If:: Performed bookings only * If:: Current time is 1 hour 15 min after Confirmed Service EndTime * Action:: set booking status as 'completed', * send messages. */ messages = 0; items = 0; { foreach (var b in LcRest.Booking.QueryPendingOfCompleteWithoutPaymentBookings(db)) { try { // Release the payment try { b.SetBookingAsCompleted(); items++; // Send messages // Notify customer and provider with an updated booking details: LcMessaging.SendBooking.For(b.bookingID).BookingCompleted(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.bookingID, "[Complete - no payment]"); messages += 2; } catch (Exception ex) { var errTitle = "Setting No-Payment Bookings as Complete after 1H15M to providers"; var errDesc = String.Format( "BookingID: {0}, Error: {1}", b.bookingID, ex.Message ); LcMessaging.NotifyError(errTitle, "/ScheduleTask", errDesc); logger.Log("Error on: " + errTitle + "; " + errDesc); } } catch (Exception ex) { logger.LogEx("Setting No-Payment Bookings as Complete 1H15M", ex); } } } logger.Log("Total of No-Payment Bookings set as Complete after 1H15M: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; /* * Check:: [8AM Review Reminder] Booking Review Reminder Next day after service at 8AM * If:: Confirmed bookings not cancelled * If:: User did not the review still * If:: Current time is 8AM on the day after the Confirmed Service EndTime * Action:: send a booking review reminder email */ /* DISABLED AS OF #844, 2016-01-26. Reminder information goes into the 'completed' email that happens sooner than before * messages = 0; * items = 0; * var confirmedPerformedCompletedStatuses = String.Join(",", new List<int> { (int)LcEnum.BookingStatus.confirmed, (int)LcEnum.BookingStatus.servicePerformed, (int)LcEnum.BookingStatus.completed }); * foreach (var b in db.Query(@" * SELECT B.BookingID, * CAST(CASE WHEN (SELECT count(*) FROM UserReviews As URP * WHERE URP.BookingID = B.BookingID * AND * URP.ProviderUserID = B.ServiceProfessionalUserID * AND * URP.PositionID = 0 * ) = 0 THEN 0 ELSE 1 END As bit) As ReviewedByProvider, * CAST(CASE WHEN (SELECT count(*) FROM UserReviews As URC * WHERE URC.BookingID = B.BookingID * AND * URC.CustomerUserID = B.ClientUserID * AND * URC.PositionID = B.JobTitleID * ) = 0 THEN 0 ELSE 1 END As bit) As ReviewedByCustomer * FROM Booking As B * INNER JOIN * CalendarEvents As E * ON B.ServiceDateID = E.Id * WHERE B.BookingStatusID IN (" + String.Join(",", new List<int> { (int)LcEnum.BookingStatus.confirmed, (int)LcEnum.BookingStatus.servicePerformed, (int)LcEnum.BookingStatus.completed }) + @") * AND * -- at 8AM hours * datepart(hh, getdate()) = 8 * AND * -- of the day after the service * Cast(dateadd(d, -1, getdate()) As Date) = Cast(E.EndTime As Date) * AND * B.MessagingLog not like '%[8AM Review Reminder]%' * ")) * { * try * { * // We need check that there was not reviews already (why send a reminder for something * // already done? just we avoid that!). * // If both users did its reviews, nothing to send * if (b.ReviewedByProvider && b.ReviewedByCustomer) * { * // Next booking * continue; * } * char messageFor = * b.ReviewedByProvider ? 'c' : * b.ReviewedByCustomer ? 'p' : * 'b'; * * // Send message * LcMessaging.SendBooking.For((int)b.BookingID).RequestToReview(); * * // Update MessagingLog for the booking * db.Execute(sqlAddBookingMessagingLog, b.BookingID, "[8AM Review Reminder]"); * * items++; * if (messageFor == 'c' || messageFor == 'p') * { * messages++; * } * else * { * messages += 2; * } * } * catch (Exception ex) * { * logger.LogEx("Booking Review Reminders Next 8AM", ex); * } * } * logger.Log("Total of Booking Review Reminders Next 8AM: {0}, messages sent: {1}", items, messages); * totalitems += items; * totalmessages += messages; */ /* * Check:: [1W Review Reminder] Booking Review Reminder 1Week after service * If:: Confirmed bookings not cancelled, non stoped manully, maybe is set as performed already * If:: User did not the review still * If:: Past 1 Week from service * Action:: send a booking review reminder email */ messages = 0; items = 0; foreach (var b in db.Query(@" SELECT B.BookingID, CAST(CASE WHEN (SELECT count(*) FROM UserReviews As URP WHERE URP.BookingID = B.BookingID AND URP.ProviderUserID = B.ServiceProfessionalUserID AND URP.PositionID = 0 ) = 0 THEN 0 ELSE 1 END As bit) As ReviewedByProvider, CAST(CASE WHEN (SELECT count(*) FROM UserReviews As URC WHERE URC.BookingID = B.BookingID AND URC.CustomerUserID = B.ClientUserID AND URC.PositionID = B.JobTitleID ) = 0 THEN 0 ELSE 1 END As bit) As ReviewedByCustomer FROM Booking As B INNER JOIN CalendarEvents As E ON B.ServiceDateID = E.Id WHERE B.BookingStatusID IN (" + String.Join(",", new List <int> { (int)LcEnum.BookingStatus.confirmed, (int)LcEnum.BookingStatus.servicePerformed, (int)LcEnum.BookingStatus.completed }) + @") AND -- at 1 Week=168 hours, after service ended (between 168 and 175 hours -6 hours of margin-) getdate() >= dateadd(hh, 168, E.EndTime) AND getdate() < dateadd(hh, 175, E.EndTime) AND B.MessagingLog not like '%[1W Review Reminder]%' ")) { try { // We need check that there was not reviews already (why send a reminder for something // already done? just we avoid that!). // If both users did its reviews, nothing to send if (b.ReviewedByProvider && b.ReviewedByCustomer) { // Next booking continue; } char messageFor = b.ReviewedByProvider ? 'c' : b.ReviewedByCustomer ? 'p' : 'b'; // Send message LcMessaging.SendBooking.For((int)b.BookingID).RequestToReviewReminder(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.BookingID, "[1W Review Reminder]"); items++; if (messageFor == 'c' || messageFor == 'p') { messages++; } else { messages += 2; } } catch (Exception ex) { logger.LogEx("Booking Review Reminders 1W", ex); } } logger.Log("Total of Booking Review Reminders 1W: {0}, messages sent: {1}", items, messages); totalitems += items; totalmessages += messages; // Ending work with database } logger.Log("Elapsed time {0}, for {1} bookings affected and {2} messages sent", DateTime.Now - elapsedTime, totalitems, totalmessages); /* * iCalendar */ DateTime partialElapsedTime = DateTime.Now; int successCalendars = 0, failedCalendars = 0; foreach (var err in LcCalendar.BulkImport()) { if (err == null) { successCalendars++; } else { failedCalendars++; logger.LogEx("Import Calendar", err); } } logger.Log("Elapsed time {0}, for {1} user calendars imported, {2} failed", DateTime.Now - partialElapsedTime, successCalendars, failedCalendars); /* * Task Ended */ logger.Log("Total Elapsed time {0}", DateTime.Now - elapsedTime); string logresult = logger.ToString(); // Finishing: save log on disk, per month rotation //try { logger.Save(); //}catch { } return(logresult); }
/// <summary> /// Check:: [1W Review Reminder] Booking Review Reminder 1Week after service /// If::Confirmed bookings not cancelled, non stoped manully, maybe is set as performed already /// If::User did not the review still /// If::Past 1 Week from service /// Action:: send a booking review reminder email /// </summary> /// <param name="logger"></param> /// <param name="db"></param> /// <returns></returns> private static TaskResult RunBookingReviewReminder1W(LcLogger logger, Database db) { var messages = 0L; var items = 0L; foreach (var b in db.Query(@" SELECT B.BookingID, CAST(CASE WHEN (SELECT count(*) FROM UserReviews As URP WHERE URP.BookingID = B.BookingID AND URP.ProviderUserID = B.ServiceProfessionalUserID AND URP.PositionID = 0 ) = 0 THEN 0 ELSE 1 END As bit) As ReviewedByProvider, CAST(CASE WHEN (SELECT count(*) FROM UserReviews As URC WHERE URC.BookingID = B.BookingID AND URC.CustomerUserID = B.ClientUserID AND URC.PositionID = B.JobTitleID ) = 0 THEN 0 ELSE 1 END As bit) As ReviewedByCustomer FROM Booking As B INNER JOIN CalendarEvents As E ON B.ServiceDateID = E.Id WHERE B.BookingStatusID IN (" + String.Join(",", new List <int> { (int)LcEnum.BookingStatus.confirmed, (int)LcEnum.BookingStatus.servicePerformed, (int)LcEnum.BookingStatus.completed }) + @") AND -- at 1 Week=168 hours, after service ended (between 168 and 175 hours -6 hours of margin-) getdate() >= dateadd(hh, 168, E.EndTime) AND getdate() < dateadd(hh, 175, E.EndTime) AND B.MessagingLog not like '%[1W Review Reminder]%' ")) { try { // We need check that there was not reviews already (why send a reminder for something // already done? just we avoid that!). // If both users did its reviews, nothing to send if (b.ReviewedByProvider && b.ReviewedByCustomer) { // Next booking continue; } char messageFor = b.ReviewedByProvider ? 'c' : b.ReviewedByCustomer ? 'p' : 'b'; // Send message LcMessaging.SendBooking.For((int)b.BookingID).RequestToReviewReminder(); // Update MessagingLog for the booking db.Execute(sqlAddBookingMessagingLog, b.BookingID, "[1W Review Reminder]"); items++; if (messageFor == 'c' || messageFor == 'p') { messages++; } else { messages += 2; } } catch (Exception ex) { logger.LogEx("Booking Review Reminders 1W", ex); } } logger.Log("Total of Booking Review Reminders 1W: {0}, messages sent: {1}", items, messages); return(new TaskResult { ItemsReviewed = items, ItemsProcessed = items, MessagesSent = messages }); }
/// <summary> /// Executes the page code of the desired method /// and returns an object with the result. /// That result must be piped to the response manually /// (or use JsonResponse). /// </summary> /// <param name="WebPage"></param> /// <returns></returns> public dynamic Run(System.Web.WebPages.WebPage WebPage) { this.WebPage = WebPage; dynamic result = null; // For all requests: // No cache WebPage.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); WebPage.Response.Cache.SetValidUntilExpires(false); WebPage.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); WebPage.Response.Cache.SetCacheability(HttpCacheability.NoCache); WebPage.Response.Cache.SetNoStore(); // No cookies WebPage.Response.Cookies.Clear(); try { switch (Request.HttpMethod.ToUpper()) { case "GET": result = Get(); break; case "POST": // By general, if everything goes fine, a 'post' must return // the http code '201:Created'. On any error will be replaced. // And can be explicitely replaced by the specific page on the // overrided 'Post()' method WebPage.Response.StatusCode = 201; result = Post(); break; case "PUT": result = Put(); break; case "DELETE": result = Delete(); break; case "OPTIONS": // Just let asp.net to response empty with the // custom headers for CORS that were set in the web.config break; default: throw new HttpException(405, String.Format("{0} is not allowed", Request.HttpMethod)); } } catch (ValidationException valEx) { result = new Dictionary <string, dynamic>(); result["errorMessage"] = LcRessources.ValidationSummaryTitle; result["errorSource"] = "validation"; var errors = new System.Collections.Hashtable(); if (!String.IsNullOrEmpty(valEx.ContainerName)) { errors[valEx.ContainerName + "." + valEx.ParamName] = valEx.Message; } errors[valEx.ParamName] = valEx.Message; result["errors"] = errors; WebPage.Response.StatusCode = 400; } catch (HttpException http) { result = new Dictionary <string, dynamic>(); WebPage.Response.StatusCode = http.GetHttpCode(); result["errorMessage"] = http.Message; if (!ModelState.IsValid) { result["errorSource"] = "validation"; result["errors"] = ModelState.Errors(); } if (WebPage.Response.StatusCode == 500) { if (ASP.LcHelpers.InDev) { result["exception"] = http; } else { LcLogger.LogAspnetError(http); } } } catch (Exception ex) { result = new Dictionary <string, dynamic>(); result["errorMessage"] = ex.Message; // Bad Format code for "Constraint" or Internal server error: WebPage.Response.StatusCode = ex is ConstraintException ? 400 : 500; if (ASP.LcHelpers.InDev) { result["exception"] = ex; } else { LcLogger.LogAspnetError(ex); } } return(result); }
private EarningsEntryReminderTask(LcLogger logger) { TaskStarted = DateTime.Now; this.logger = logger; }