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); // Send Email too LcMessaging.NotifyError("Catch at logger", Request.RawUrl, logger.ToString()); } catch { } logger.Save(); } }
public static void checkAccountIsConfirmed(string username) { // #454: User is auto-logged on registering, allowing it to do the Onboarding, // But next times, it is required to confirm email before logged. // Since we set IsConfirmed as true on database to let 'auto-logging on register', // we must check for the existance of a confirmation token: var userId = WebSecurity.GetUserId(username); using (var db = new LcDatabase()) { string token = LcAuth.GetConfirmationToken(userId); if (userId > -1 && !string.IsNullOrWhiteSpace(token)) { // Resend confirmation mail var confirmationUrl = LcUrl.LangUrl + "Account/Confirm/?confirmationCode=" + Uri.EscapeDataString(token ?? ""); var isProvider = (bool)(db.QueryValue("SELECT IsProvider FROM users WHERE UserID=@0", userId) ?? false); if (isProvider) { LcMessaging.SendWelcomeProvider(userId, username); } else { LcMessaging.SendWelcomeCustomer(userId, username); } /// http 409:Conflict throw new HttpException(409, "Your account has not yet been confirmed. Please check your inbox and spam folders and click on the e-mail sent."); } } }
public static bool Login(string email, string password, bool persistCookie = false) { if (IsAccountLockedOut(email)) { throw new ConstraintException(AccountLockedErrorMessage); } // Navigate back to the homepage and exit var logged = WebSecurity.Login(email, password, persistCookie); if (logged) { LcData.UserInfo.RegisterLastLoginTime(0, email); // mark the user as logged in via a normal account, // as opposed to via an OAuth or OpenID provider. System.Web.HttpContext.Current.Session["OAuthLoggedIn"] = false; } else { // Per issue #982, HIPAA rules: // Check if, by failling this login attempt, the user gets it's account locked-out if (IsAccountLockedOut(email)) { // then, notify us LcMessaging.NotifyLockedAccount(email, WebSecurity.GetUserId(email), DateTime.Now); // Rather than communicate a 'invalid user password' let the user know that now it's user // is locked out due to many unsuccessful attempts (preventing from try again something that, by sure, will be locked, // and avoiding misperception of 6 allowed attempts). throw new ConstraintException(AccountLockedErrorMessage); } } return(logged); }
public static void RequestBackgroundCheck(int userId, int backgroundCheckId) { using (var db = Database.Open("sqlloco")) { // Save request in database as 'pending' db.Execute(@" INSERT INTO UserBackgroundCheck ( UserID, BackgroundCheckID, StatusID, LastVerifiedDate, CreatedDate, ModifiedDate, ModifiedBy ) VALUES ( @0, @1, 1, --status: pending getdate(), getdate(), getdate(), 'sys' ) -- Check Alert EXEC TestAlertBackgroundCheck @0 ", userId, backgroundCheckId); // Send email to loconomics LcMessaging.SendMail("*****@*****.**", "[Action Required] Background check request", LcMessaging.ApplyTemplate(LcUrl.LangPath + "Email/EmailBackgroundCheckRequest/", new Dictionary <string, object> { { "ProviderUserID", userId }, { "BackgroundCheckID", backgroundCheckId } })); } }
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> /// Creat a thread with an initial message. /// Send email to the recipient, but without copy to sender. /// </summary> /// <param name="CustomerUserID"></param> /// <param name="FreelancerUserID"></param> /// <param name="JobTitleID"></param> /// <param name="Subject"></param> /// <param name="BodyText"></param> /// <param name="SentByFreelancer"></param> public static int PostInquiry(int CustomerUserID, int FreelancerUserID, int JobTitleID, string Subject, string BodyText, int SentByUserID) { // Validate user can send it (its in the thread) if (SentByUserID != CustomerUserID && SentByUserID != FreelancerUserID) { // Not allowed, quick return, nothing done: return(0); } var clientEmail = UserProfile.GetEmail(CustomerUserID); var serviceProfessionalEmail = UserProfile.GetEmail(FreelancerUserID); if (clientEmail != null && serviceProfessionalEmail != null) { bool SentByFreelancer = SentByUserID == FreelancerUserID; int typeID = SentByFreelancer ? 22 : 1; int threadID = LcMessaging.CreateThread(CustomerUserID, FreelancerUserID, JobTitleID, Subject, typeID, BodyText, SentByUserID); // From a REST API, a copy is Not send to the original user, as in the general API, so just // send an email to the recipient // NOTE: the Kind possible values are in the template. if (SentByFreelancer) { // TODO: i18n LcMessaging.SendMail(clientEmail, "A Message From a Loconomics Freelancer", LcMessaging.ApplyTemplate(LcUrl.LangPath + "Email/EmailInquiry/", new Dictionary <string, object> { { "ThreadID", threadID }, { "Kind", 4 }, { "RequestKey", LcMessaging.SecurityRequestKey }, { "EmailTo", clientEmail } }), replyTo: serviceProfessionalEmail ); } else { LcMessaging.SendMail(serviceProfessionalEmail, "A Message From a Loconomics Client", LcMessaging.ApplyTemplate(LcUrl.LangPath + "Email/EmailInquiry/", new Dictionary <string, object> { { "ThreadID", threadID }, { "Kind", 1 }, { "RequestKey", LcMessaging.SecurityRequestKey }, { "EmailTo", serviceProfessionalEmail } }), replyTo: clientEmail ); } return(threadID); } return(0); }
public static void SendRegisterUserEmail(RegisteredUser user) { // Sent welcome email (if there is a confirmationUrl and token values, the email will contain it to perform the required confirmation) if (user.IsProvider) { LcMessaging.SendWelcomeProvider(user.UserID, user.Email); } else { LcMessaging.SendWelcomeCustomer(user.UserID, user.Email); } }
/// <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 bool ProcessUser(int userID) { ItemsReviewed += 1; try { LcMessaging.SendEarningsEntryReminder(userID, LcRest.UserProfile.GetEmail(userID)); ItemsProcessed += 1; } catch (Exception ex) { logger.LogEx("Earnings Entry Reminder for userID:" + userID, ex); } return(true); }
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 }); }
/// <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 }); }
public static int SubmitPhoto(UserLicenseCertification item, string originalFileName, Stream photo) { // For updates, needs to remove previous file if (item.userLicenseCertificationID > 0) { var oldItem = Get(item.userID, item.jobTitleID, item.languageID, item.userLicenseCertificationID); if (oldItem == null) { // Not found: return(0); } if (!String.IsNullOrEmpty(oldItem.submittedImageLocalURL)) { var localPath = oldItem.submittedImageLocalURL.Replace(LcUrl.AppUrl, ""); localPath = HttpContext.Current.Server.MapPath(LcUrl.RenderAppPath + localPath); File.Delete(localPath); } } // File name with special prefix var autofn = Guid.NewGuid().ToString().Replace("-", ""); string fileName = photoPrefix + autofn + (System.IO.Path.GetExtension(originalFileName) ?? ".jpg"); string virtualPath = LcUrl.RenderAppPath + LcData.Photo.GetUserPhotoFolder(item.userID); var path = HttpContext.Current.Server.MapPath(virtualPath); item.submittedImageLocalURL = LcUrl.AppUrl + LcData.Photo.GetUserPhotoFolder(item.userID) + fileName; // Check folder or create if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } using (var file = File.Create(path + fileName)) { photo.CopyTo(file); } // TODO: i18n var msg = "UserID: " + item.userID + " submitted a photo of their License/Certification to being verified and added. It can be found in the FTP an folder: " + virtualPath; // TODO create config value var email = "*****@*****.**"; LcMessaging.SendMail(email, "License/Certification Verification Request", msg); return(Set(item)); }
static public void Set(UserJobTitleServiceAttributes serviceAttributes) { // Validate // Get all attributes that applies (we avoid save selected attributes that does not apply // to the job title). var validAttributes = ServiceAttribute.GetGroupedJobTitleAttributes(serviceAttributes.jobTitleID, serviceAttributes.languageID, serviceAttributes.countryID); var indexedValidAttributes = new Dictionary <int, HashSet <int> >(); // Check that there is almost one value for required categories, or show error foreach (var attCat in validAttributes) { if (attCat.requiredInput && ( !serviceAttributes.serviceAttributes.ContainsKey(attCat.serviceAttributeCategoryID) || serviceAttributes.serviceAttributes[attCat.serviceAttributeCategoryID].Count == 0)) { throw new ValidationException(String.Format(requiredAttCatError, attCat.name), attCat.serviceAttributeCategoryID.ToString(), "serviceAttributes"); } indexedValidAttributes.Add(attCat.serviceAttributeCategoryID, new HashSet <int>(attCat.serviceAttributes.Select(x => x.serviceAttributeID))); } // Save data using (var db = new LcDatabase()) { // Transaction db.Execute("BEGIN TRANSACTION"); // First, remove all current attributes, replaced by the new set db.Execute(sqlDelAllAttributes, serviceAttributes.userID, serviceAttributes.jobTitleID, serviceAttributes.languageID, serviceAttributes.countryID); // Add new ones, if they are valid foreach (var cat in serviceAttributes.serviceAttributes) { if (indexedValidAttributes.ContainsKey(cat.Key)) { foreach (var att in cat.Value) { if (indexedValidAttributes[cat.Key].Contains(att)) { // Add to database db.Execute(sqlInsertAttribute, serviceAttributes.userID, serviceAttributes.jobTitleID, cat.Key, att, serviceAttributes.languageID, serviceAttributes.countryID); } // else JUST DISCARD SILENTLY INVALID ATTID } } // else JUST DISCARD SILENTLY INVALID CATID } // Register user proposed new attributes: foreach (var cat in serviceAttributes.proposedServiceAttributes) { // Category must exists, even if the attribute is new. if (indexedValidAttributes.ContainsKey(cat.Key)) { foreach (var attName in cat.Value) { if (String.IsNullOrWhiteSpace(attName)) { continue; } // Clean-up, preparation of the new name var newAttName = attName.Capitalize().Replace(",", ""); // Register new attribute int serviceAttributeID = db.QueryValue(sqlRegisterNewAttribute, serviceAttributes.languageID, serviceAttributes.countryID, null, // sourceID newAttName, null, // description serviceAttributes.jobTitleID, serviceAttributes.userID, false, // Initially not approved cat.Key // categoryID ); // Set for the user: db.Execute(sqlInsertAttribute, serviceAttributes.userID, serviceAttributes.jobTitleID, cat.Key, serviceAttributeID, serviceAttributes.languageID, serviceAttributes.countryID); } } } if (serviceAttributes.proposedServiceAttributes.Count > 0) { LcMessaging.NotifyNewServiceAttributes(serviceAttributes.userID, serviceAttributes.jobTitleID, serviceAttributes.proposedServiceAttributes); } // Since ExperienceLevel is not a service category anymore else an independent table, we need // specific code to save its data. if (serviceAttributes.experienceLevelID > 0) { db.Execute(sqlSetExpLevel, serviceAttributes.userID, serviceAttributes.jobTitleID, serviceAttributes.languageID, serviceAttributes.countryID, serviceAttributes.experienceLevelID); } else { db.Execute(sqlDelExpLevel, serviceAttributes.userID, serviceAttributes.jobTitleID, serviceAttributes.languageID, serviceAttributes.countryID); } // Check alert db.Execute("EXEC TestAlertPositionServices @0, @1", serviceAttributes.userID, serviceAttributes.jobTitleID); // Ends transaction (very important for the delete-insert attributes part, but it guarantees that all or nothing): db.Execute("COMMIT TRANSACTION"); } }
/// <summary> /// Convert a user record with 'Not Enabled Account' into a standard enabled account. See IsUserButNotEnabledAccount /// for more info, and check that value before call this to prevent an error when user has an enabled account. /// This supports the cases /// - User with status of 'serviceProfessionalClient': the user has an account created as client by a service professional. /// - User with status of 'subscriber': the user submitted it's email through a Lead Generation API to get in touch with newsletters or /// to reference a service professional. /// /// For the conversion, we need support next actions/requests: /// - A: We need to communicate that specific situation (error message), generate a confirmation code /// for the existent user, send email to let him to confirm that he/she owns the given e-mail. /// - B: On returning here after request/response A, a confirmation code is being provided and we must proceed /// by checking the confirmation code and, on success, enable account (change status), update the membership password and /// continue with a valid set of LoginResult. External code should allow user to update any additional account data. /// </summary> /// <param name="userID"></param> /// <param name="email"></param> /// <param name="password"></param> /// <param name="returnProfile"></param> /// <returns></returns> private static LoginResult SignupANotEnabledAccount(int userID, string email, string password, bool returnProfile, int accountStatusID, bool isOrganization) { // Get confirmation code, if any var confirmationCode = Request["confirmationCode"]; // Prepare error message var errTpl = ""; if (accountStatusID == (int)LcEnum.AccountStatus.serviceProfessionalClient) { errTpl = UserIsServiceProfessionalClientMessage; } else if (accountStatusID == (int)LcEnum.AccountStatus.subscriber) { errTpl = UserIsSubscriberMessage; } else { throw new Exception("[[[Not allowed]]]"); } var errMsg = String.Format(errTpl, email); // Action/Request A: Create confirmation code if (String.IsNullOrEmpty(confirmationCode)) { // To generate a confirmation code (creates the Membership record, that does not exists still) // this needs a random password (we still didn't verified the user, so do NOT trust on the given password). // NOTE: since this can be attempted several time by the user, and next attempts will fail because the Membership // record will exists already, just double check and try creation only if record don't exists: if (!LcAuth.HasMembershipRecord(userID)) { WebSecurity.CreateAccount(email, LcAuth.GeneratePassword(), true); } StartOnboardingForUser(userID); // send email to let him to confirm it owns the given e-mail LcMessaging.SendWelcomeCustomer(userID, email); // Not valid after all, just communicate was was done and needs to do to active its account: throw new HttpException(409, errMsg); } // Action/Request B: confirm confirmation code else { // If confirmation token is valid, enable account and reset password if (LcAuth.GetConfirmationToken(userID) == confirmationCode) { // We know is valid, we can update the accountStatus to be an standard/enabled account // and that will allow to set the account as confirmed using (var db = new LcDatabase()) { db.Execute("UPDATE users SET accountStatusID = @1 WHERE UserID = @0", userID, LcEnum.AccountStatus.active); } // now we can confirm (we already know the code is valid, it will just double check and update database) LcAuth.ConfirmAccount(confirmationCode); // set the password provided by the user. Trick: we need to generate a reset token in order to set the password. var token = WebSecurity.GeneratePasswordResetToken(email); LcAuth.ResetPassword(token, password); // Left continue with profile data update.. } else { // RE-send email to let him to confirm it owns the given e-mail LcMessaging.SendWelcomeCustomer(userID, email); throw new HttpException(409, errMsg); } } // We need a logged object, and additionally a double check is performed (so we ensure setting the password process worked). return(Login(email, password, false, returnProfile, false)); }
/// <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> /// 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> /// Signup with fields: /// - email [required] /// - password [required when no facebookUserID is given] /// - facebookUserID [optional] /// - countryID [optional defaults to COUNTRY_CODE_USA] /// - profileType [optional defaults to client] /// - utm [optional, not a named form parameter but the whole query string] /// - firstName [optional except atBooking] /// - lastName [optional except atBooking] /// - phone [optional except atBooking] /// - returnProfile [optional defaults to false] Returns the user profile in a property of the result /// - atBooking [optional] /// - isOrganization [optional] Default false /// </summary> /// <param name="page"></param> /// <returns></returns> public static LoginResult Signup(WebPage page) { page.Validation.RequireField("email", "[[[You must specify an email.]]]"); // Username is an email currently, so need to be restricted page.Validation.Add("email", Validator.Regex(LcValidators.EmailAddressRegexPattern, "[[[The email is not valid.]]]")); // First data var profileTypeStr = Request.Form["profileType"] ?? ""; var isServiceProfessional = SERVICE_PROFESSIONAL_TYPE == profileTypeStr.ToUpper(); var isClient = !isServiceProfessional; var facebookUserID = Request.Form["facebookUserID"].AsLong(0); var facebookAccessToken = Request.Form["facebookAccessToken"]; var email = Request.Form["email"]; var atBooking = Request.Form["atBooking"].AsBool(); var isOrganization = Request.Form["isOrganization"].AsBool(); // // Conditional validations // Facebook var useFacebookConnect = facebookUserID > 0 && !String.IsNullOrEmpty(facebookAccessToken); if (!useFacebookConnect) { page.Validation.RequireField("password", "[[[You must specify a password.]]]"); // We manually validate if a password was given, in order to prevent // showing up the validation format message additionally to the 'required password' message if (!String.IsNullOrWhiteSpace(Request.Form["password"])) { page.Validation.Add("password", new PasswordValidator()); } } else { var prevFbUser = LcAuth.GetFacebookUser(facebookUserID); if (prevFbUser != null) { throw new HttpException(409, "[[[Facebook account already connected. Sign in.]]]"); } } // For a signup at a client booking, we require more fields if (atBooking) { page.Validation.RequireField("phone", "[[[You must specify your mobile phone number.]]]"); page.Validation.RequireField("firstName", "[[[You must specify your first name.]]]"); page.Validation.RequireField("lastName", "[[[You must specify your last name.]]]"); } if (page.Validation.IsValid()) { // TODO To use countryCode for a more 'open' public REST API, where 'code' is a well know ISO 2-letters CODE //var countryCode = Request.Form["countryCode"] ?? "US"; var countryID = Request.Form["countryID"].AsInt(COUNTRY_CODE_AU); // Autogenerated password (we need to save one) on facebook connect: var password = useFacebookConnect ? LcAuth.GeneratePassword() : Request.Form["password"]; var firstName = Request.Form["firstName"]; var lastName = Request.Form["lastName"]; var phone = Request.Form["phone"]; var returnProfile = Request.Form["returnProfile"].AsBool(); var utm = Request.Url.Query; LoginResult logged = null; // If the user exists, try to log-in with the given password, // becoming a provider if that was the requested profileType and follow as // a normal login. // If the password didn't match, throw a sign-up specific error (email in use) // Otherwise, just register the user. if (LcAuth.ExistsEmail(email)) { // We query the user with that email var userID = WebSecurity.GetUserId(email); var user = LcRest.UserProfile.Get(userID); // There are special cases when a user is registered, but never has accepted TOU or created a password (Not Enabled Account), // and is possible for that user to become an regular/enabled account. if (IsUserButNotEnabledAccount(user)) { logged = SignupANotEnabledAccount(userID, email, password, returnProfile, user.accountStatusID, isOrganization); } else { // If the email exists, we try to log-in using the provided password, to don't bother with "e-mail in use" error // if the user provides the correct credentials (maybe just don't remember he/she has already an account; make it easy for them // to return). // Try Login try { logged = Login(email, password, false, returnProfile, true); userID = logged.userID; // Ensure we set-up // as a professional if requested // Next code will throw exception on error if (isServiceProfessional) { LcAuth.BecomeProvider(userID); } } catch (HttpException) { // Not valid log-in, throw a 'email exists' error with Conflict http code throw new HttpException(409, "[[[E-mail address is already in use.]]]"); } } // Update account data with the extra information. using (var db = new LcDatabase()) { db.Execute(@" UPDATE users SET firstName = coalesce(@1, firstName), lastName = coalesce(@2, lastName), mobilePhone = coalesce(@3, mobilePhone), isOrganization = @4 WHERE userID = @0 ", userID, firstName, lastName, phone, isOrganization); // Create a home address record almost with the country var home = LcRest.Address.GetHomeAddress(userID); home.countryCode = LcRest.Locale.GetCountryCodeByID(countryID); home.countryID = countryID; LcRest.Address.SetAddress(home); StartOnboardingForUser(userID); } // SIGNUP LcMessaging.SendMail("*****@*****.**", "Sign-up", String.Format(@" <html><body><h3>Sign-up.</h3> <strong>This user was already in the database, is re-registering itself again!</strong><br/> <dl> <dt>Profile:</dt><dd>{0}</dd> <dt>First Name:</dt><dd>{1}</dd> <dt>Last Name:</dt><dd>{2}</dd> <dt>Country:</dt><dd>{5}</dd> <dt>Email:</dt><dd>{3}</dd> <dt>UserID:</dt><dd>{4}</dd> <dt>Phone:</dt><dd>{6}</dd> </dl> </body></html> ", profileTypeStr, firstName, lastName, email, logged.userID, countryID, phone)); return(logged); } else { if (useFacebookConnect) { // Verify Facebook ID and accessToken contacting to Facebook Servers if (LcFacebook.GetUserFromAccessToken(facebookUserID.ToString(), facebookAccessToken) == null) { throw new HttpException(400, "[[[Facebook account does not exists.]]]"); } } var registered = LcAuth.RegisterUser(email, firstName, lastName, password, isServiceProfessional, utm, -1, null, phone, null, countryID, isOrganization); // Create a home address record almost with the country var home = LcRest.Address.GetHomeAddress(registered.UserID); home.countryCode = LcRest.Locale.GetCountryCodeByID(countryID); home.countryID = countryID; LcRest.Address.SetAddress(home); if (useFacebookConnect) { // Register connection between the new account and the Facebook account LcAuth.ConnectWithFacebookAccount(registered.UserID, facebookUserID); } // Welcome and confirmation e-mail LcAuth.SendRegisterUserEmail(registered); // SIGNUP LcMessaging.SendMail("*****@*****.**", "Sign-up", String.Format(@" <html><body><h3>Sign-up.</h3> <dl> <dt>Profile:</dt><dd>{0}</dd> <dt>First Name:</dt><dd>{1}</dd> <dt>Last Name:</dt><dd>{2}</dd> <dt>Country:</dt><dd>{5}</dd> <dt>Email:</dt><dd>{3}</dd> <dt>UserID:</dt><dd>{4}</dd> <dt>Phone:</dt><dd>{6}</dd> </dl> </body></html> ", profileTypeStr, firstName, lastName, email, registered.UserID, countryID, phone)); // Auto login: return(Login(email, password, false, returnProfile, true)); } } else { // Bad request, input data incorrect because of validation rules throw new HttpException(400, LcRessources.ValidationSummaryTitle); } }
/// <summary> /// Signup with fields: /// - email [required] /// - password [required when no facebookUserID is given] /// - facebookUserID [optional] /// - countryID [optional defaults to COUNTRY_CODE_USA] /// - profileType [optional defaults to client] /// - utm [optional, not a named form parameter but the whole query string] /// - firstName [optional for professionals, required for clients] /// - lastName [optional for professionals, required for clients] /// - postalCode [optional] /// - referralCode [optional] /// - device [optional] /// - phone [optional for professionals, required for clients] /// - returnProfile [optional defaults to false] Returns the user profile in a property of the result /// </summary> /// <param name="page"></param> /// <returns></returns> public static LoginResult Signup(WebPage page) { page.Validation.RequireField("email", "You must specify an email."); // Username is an email currently, so need to be restricted page.Validation.Add("email", Validator.Regex(LcValidators.EmailAddressRegexPattern, "The email is not valid.")); // First data var profileTypeStr = Request.Form["profileType"] ?? ""; var isServiceProfessional = SERVICE_PROFESSIONAL_TYPE == profileTypeStr.ToUpper(); var isClient = !isServiceProfessional; var facebookUserID = Request.Form["facebookUserID"].AsLong(0); var facebookAccessToken = Request.Form["facebookAccessToken"]; var email = Request.Form["email"]; // // Conditional validations // Facebook var useFacebookConnect = facebookUserID > 0 && !String.IsNullOrEmpty(facebookAccessToken); if (!useFacebookConnect) { page.Validation.RequireField("password", "You must specify a password."); // We manually validate if a password was given, in order to prevent // showing up the validation format message additionally to the 'required password' message if (!String.IsNullOrWhiteSpace(Request.Form["password"])) { page.Validation.Add("password", new PasswordValidator()); } } else { var prevFbUser = LcAuth.GetFacebookUser(facebookUserID); if (prevFbUser != null) { throw new HttpException(409, "Facebook account already connected. Sign in."); } } // Profile Type if (isClient) { page.Validation.RequireField("phone", "You must specify your mobile phone number."); page.Validation.RequireField("firstName", "You must specify your first name."); page.Validation.RequireField("lastName", "You must specify your last name."); } if (page.Validation.IsValid()) { var postalCode = Request.Form["postalCode"]; // TODO To use countryCode for a more 'open' public REST API, where 'code' is a well know ISO 2-letters CODE //var countryCode = Request.Form["countryCode"] ?? "US"; var countryID = Request.Form["countryID"].AsInt(COUNTRY_CODE_USA); // Postal code is Optional if (!String.IsNullOrEmpty(postalCode)) { // Validate postal code before continue var add = new LcRest.Address { postalCode = postalCode, //countryCode = countryCode countryID = countryID }; if (!LcRest.Address.AutosetByCountryPostalCode(add)) { // bad postal code page.ModelState.AddError("postalCode", "Invalid postal code"); throw new HttpException(400, LcRessources.ValidationSummaryTitle); } } // Autogenerated password (we need to save one) on facebook connect: var password = useFacebookConnect ? LcAuth.GeneratePassword() : Request.Form["password"]; var firstName = Request.Form["firstName"]; var lastName = Request.Form["lastName"]; var referralCode = Request.Form["referralCode"]; var device = Request.Form["device"]; var phone = Request.Form["phone"]; var returnProfile = Request.Form["returnProfile"].AsBool(); var utm = Request.Url.Query; LoginResult logged = null; // If the user exists, try to log-in with the given password, // becoming a provider if that was the requested profileType and follow as // a normal login. // If the password didn't match, throw a sign-up specific error (email in use) // Otherwise, just register the user. if (LcAuth.ExistsEmail(email)) { // If the email exists, we try to log-in using the provided password, to don't bother with "e-mail in use" error // if the user provides the correct credentials (maybe just don't remember he/she has already an account; make it easy for them // to return). // BUT we have a special situation that needs extra checks: // CLIENT--CONFIRMATION LOGIC // The email can exists because the user has an account created as client by a service professional: // - A: On that cases, we need to communicate that specific situation (error message), generate a confirmation code // for the existent user, send email to let him to confirm it owns the given e-mail. // - B: On returning here after point A, a confirmation code is provided and we must proceed // by checking the confirmation code and, on success, unlock and update the membership password and // continue updating any given data. var userID = WebSecurity.GetUserId(email); var user = LcRest.UserProfile.Get(userID); if (user.accountStatusID != (int)LcEnum.AccountStatus.serviceProfessionalClient) { // NOT a client, just standard sign-up that requires verify the email/password or fail // Try Login try { logged = Login(email, password, false, returnProfile, true); userID = logged.userID; // throw exception on error if (isServiceProfessional) { LcAuth.BecomeProvider(userID); } } catch (HttpException) { // Not valid log-in, throw a 'email exists' error with Conflict http code throw new HttpException(409, "E-mail address is already in use."); } } else { // CLIENT--CONFIRMATION LOGIC // The email can exists because the user has an account created as client by a service professional: // - A: On that cases, we need to communicate that specific situation (error message), generate a confirmation code // for the existent user, send email to let him to confirm it owns the given e-mail. // - B: On returning here after point A, a confirmation code is provided and we must proceed // by checking the confirmation code and, on success, unlock and update the membership password and // continue updating any given data. var confirmationCode = Request["confirmationCode"]; var errMsg = String.Format(@"We see one of our service professionals has already scheduled services for you in the past. We've just sent an invitation to create your account to {0}. Please follow its instructions. We can't wait to get you on board!", email ); if (String.IsNullOrEmpty(confirmationCode)) { // Point A: create confirmation code // generate a confirmation code (creates the Membership record, that does not exists still since is as just a client) // this needs a random password (we still didn't verified the user, so do NOT trust on the given password). // NOTE: since this can be attempted several time by the user, and next attempts will fail because the Membership // record will exists already, just double check and try creation only if record don't exists: if (!LcAuth.HasMembershipRecord(userID)) { WebSecurity.CreateAccount(email, LcAuth.GeneratePassword(), true); } // send email to let him to confirm it owns the given e-mail LcMessaging.SendWelcomeCustomer(userID, email); // Not valid after all, just communicate was was done and needs to do to active its account: throw new HttpException(409, errMsg); } else { // Point B: confirm confirmation code if (LcAuth.GetConfirmationToken(userID) == confirmationCode) { // We know is valid, we can update the accountStatus to not be any more a "service professional's client" // and that will allow to set the account as confirmed using (var db = new LcDatabase()) { db.Execute("UPDATE users SET accountStatusID = @1 WHERE UserID = @0", userID, LcEnum.AccountStatus.active); } // now we can confirm (we already know the code is valid, it will just double check and update database) LcAuth.ConfirmAccount(confirmationCode); // set the password provided by the user. Trick: we need to generate a reset token in order to set the password. var token = WebSecurity.GeneratePasswordResetToken(email); LcAuth.ResetPassword(token, password); // Left continue with profile data update.. } else { // RE-send email to let him to confirm it owns the given e-mail LcMessaging.SendWelcomeCustomer(userID, email); throw new HttpException(409, errMsg); } } // We need a logged object, and additionally a double check is performed (so we ensure setting the password process worked). logged = Login(email, password, false, returnProfile, false); } // Update account data with the extra information. using (var db = new LcDatabase()) { db.Execute(@" UPDATE users SET firstName = coalesce(@1, firstName), lastName = coalesce(@2, lastName), mobilePhone = coalesce(@3, mobilePhone), signupDevice = coalesce(@4, signupDevice) WHERE userID = @0 ", userID, firstName, lastName, phone, device); if (!String.IsNullOrEmpty(postalCode)) { var address = LcRest.Address.GetHomeAddress(userID); if (address.postalCode != postalCode) { address.postalCode = postalCode; //address.countryCode = countryCode; address.countryCode = LcRest.Locale.GetCountryCodeByID(countryID); address.countryID = countryID; LcRest.Address.SetAddress(address); } } } // SIGNUP LcMessaging.SendMail("*****@*****.**", "Sign-up", String.Format(@" <html><body><h3>Sign-up.</h3> <strong>This user was already in the database, is re-registering itself again!</strong><br/> <dl> <dt>Profile:</dt><dd>{0}</dd> <dt>First Name:</dt><dd>{1}</dd> <dt>Last Name:</dt><dd>{2}</dd> <dt>Postal code:</dt><dd>{3}</dd> <dt>Country:</dt><dd>{9}</dd> <dt>Referral code:</dt><dd>{4}</dd> <dt>Device:</dt><dd>{5}</dd> <dt>Phone:</dt><dd>{6}</dd> <dt>Email:</dt><dd>{7}</dd> <dt>UserID:</dt><dd>{8}</dd> </dl> </body></html> ", profileTypeStr, firstName, lastName, postalCode, referralCode, device, phone, email, logged.userID, countryID)); return(logged); } else { if (useFacebookConnect) { // Verify Facebook ID and accessToken contacting to Facebook Servers if (LcFacebook.GetUserFromAccessToken(facebookUserID.ToString(), facebookAccessToken) == null) { throw new HttpException(400, "Facebook account does not exists."); } } var registered = LcAuth.RegisterUser(email, firstName, lastName, password, isServiceProfessional, utm, -1, null, phone, device); if (!String.IsNullOrEmpty(postalCode)) { // Set address var address = LcRest.Address.GetHomeAddress(registered.UserID); address.postalCode = postalCode; //address.countryCode = countryCode; address.countryCode = LcRest.Locale.GetCountryCodeByID(countryID); address.countryID = countryID; LcRest.Address.SetAddress(address); } if (useFacebookConnect) { // Register connection between the new account and the Facebook account LcAuth.ConnectWithFacebookAccount(registered.UserID, facebookUserID); } // Welcome and confirmation e-mail LcAuth.SendRegisterUserEmail(registered); // SIGNUP LcMessaging.SendMail("*****@*****.**", "Sign-up", String.Format(@" <html><body><h3>Sign-up.</h3> <dl> <dt>Profile:</dt><dd>{0}</dd> <dt>First Name:</dt><dd>{1}</dd> <dt>Last Name:</dt><dd>{2}</dd> <dt>Postal code:</dt><dd>{3}</dd> <dt>Country:</dt><dd>{9}</dd> <dt>Referral code:</dt><dd>{4}</dd> <dt>Device:</dt><dd>{5}</dd> <dt>Phone:</dt><dd>{6}</dd> <dt>Email:</dt><dd>{7}</dd> <dt>UserID:</dt><dd>{8}</dd> </dl> </body></html> ", profileTypeStr, firstName, lastName, postalCode, referralCode, device, phone, email, registered.UserID, countryID)); // Auto login: return(Login(email, password, false, returnProfile, true)); } } else { // Bad request, input data incorrect because of validation rules throw new HttpException(400, LcRessources.ValidationSummaryTitle); } }
/// <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); }
public static int PostInquiry(int ThreadID, string BodyText, int SentByUserID) { // Get Thread info var thread = Thread.Get(ThreadID); // Validate user can send it (its in the thread) if (thread == null || ( SentByUserID != thread.clientUserID && SentByUserID != thread.serviceProfessionalUserID )) { // Not allowed, quick return, nothing done: return(0); } var clientEmail = UserProfile.GetEmail(thread.clientUserID); var serviceProfessionalEmail = UserProfile.GetEmail(thread.serviceProfessionalUserID); if (clientEmail != null && serviceProfessionalEmail != null) { bool SentByFreelancer = SentByUserID == thread.serviceProfessionalUserID; var firstMessage = GetFirstThreadMessage(ThreadID); var firstSentByFreelancer = firstMessage.sentByUserID == thread.serviceProfessionalUserID; // ThreadStatus: 1=respond, 2=responded // MessageType: 1=customer inquiry, 3=provider answer, 22=provider inquiry, 23=customer answer var statusID = 0; var typeID = 0; if (firstSentByFreelancer) { if (SentByFreelancer) { // Freelancer is asking again statusID = (int)LcMessaging.MessageThreadStatus.Respond; typeID = (int)LcMessaging.MessageType.ProfessionalInquiry; } else { // Client answered statusID = (int)LcMessaging.MessageThreadStatus.Responded; typeID = (int)LcMessaging.MessageType.ClientResponseToInquiry; } } else { if (SentByFreelancer) { // Freelancer answered statusID = (int)LcMessaging.MessageThreadStatus.Responded; typeID = (int)LcMessaging.MessageType.ProfessionalResponseToInquiry; } else { // Client is asking again statusID = (int)LcMessaging.MessageThreadStatus.Respond; typeID = (int)LcMessaging.MessageType.ClientInquiry; } } int messageID = LcMessaging.CreateMessage(ThreadID, statusID, typeID, BodyText, SentByUserID); // From a REST API, a copy is Not send to the original user, as in the general API, so just // send an email to the recipient // NOTE: the Kind possible values are in the template. if (SentByFreelancer) { // NOTE: Message from freelancer to client, answering an inquiry started by the client. // TODO: i18n LcMessaging.SendMail(clientEmail, "A Message From a Loconomics Freelancer", LcMessaging.ApplyTemplate(LcUrl.LangPath + "Email/EmailInquiry/", new Dictionary <string, object> { { "ThreadID", ThreadID }, { "MessageID", messageID }, { "Kind", firstSentByFreelancer ? 6 : 2 }, { "RequestKey", LcMessaging.SecurityRequestKey }, { "EmailTo", clientEmail } }), replyTo: serviceProfessionalEmail ); } else { // NOTE: Copy to the author. The author is a freelancer, answering to a client that started the inquiry. // TODO: i18n LcMessaging.SendMail(serviceProfessionalEmail, "A Message From a Loconomics Client", LcMessaging.ApplyTemplate(LcUrl.LangPath + "Email/EmailInquiry/", new Dictionary <string, object> { { "ThreadID", ThreadID }, { "MessageID", messageID }, { "Kind", firstSentByFreelancer ? 5 : 3 }, { "RequestKey", LcMessaging.SecurityRequestKey }, { "EmailTo", serviceProfessionalEmail } }), replyTo: clientEmail ); } return(messageID); } // no emails, users don't exists or inactive return(0); }
/// <summary> /// Process a request to create a user job title given a jobTitleID or as /// fallback a validated and sanitized jobTitleName (pass in GetValidatedJobTitleName result) /// </summary> /// <param name="userID"></param> /// <param name="jobTitleID"></param> /// <param name="jobTitleName"></param> /// <returns></returns> public dynamic Create(int userID, int jobTitleID, string jobTitleName) { var jobTitleExists = false; if (jobTitleID == 0) { // Look-up/new-job-title version: it's possible that the user wrotes a // job title name without pick-up one from the list, we look-up for that in database // for a jobTitleID, // and may not exists, so we try to create a new one with a template. // Name for the job title is required if (String.IsNullOrEmpty(jobTitleName)) { throw new HttpException(400, "A Job Title is required"); } // Search var jobTitle = LcRest.PublicJobTitle.AutocompleteSearch(jobTitleName, LcRest.Locale.Current).FirstOrDefault(); if (jobTitle != null) { // Use the first one jobTitleID = jobTitle.value; jobTitleExists = true; } else { // Create a new job-title based on the given name #650 jobTitleID = LcData.JobTitle.CreateJobTitleByName(jobTitleName, LcRest.Locale.Current.languageID, LcRest.Locale.Current.countryID, userID); // Check well know custom error codes if (jobTitleID == -1) { throw new HttpException(400, String.Format("The Job Title '{0}' is not allowed.", jobTitleName)); } LcMessaging.NotifyNewJobTitle(jobTitleName, jobTitleID); jobTitleExists = true; } } else { // Double check that the job title exists jobTitleExists = LcRest.PublicJobTitle.Get(jobTitleID, LcRest.Locale.Current) != null; } if (jobTitleID > 0 && jobTitleExists) { // Read data; It stops on not valid: var data = GetValidatedItemBodyInput(); LcData.JobTitle.InsertUserJobTitle( userID, jobTitleID, data.policyID, data.intro, data.instantBooking, data.collectPaymentAtBookMeButton, LcRest.Locale.Current.languageID, LcRest.Locale.Current.countryID ); // If user is just a client, needs to become a professional var user = LcRest.UserProfile.Get(userID); if (!user.isServiceProfessional) { LcAuth.BecomeProvider(userID); // Set onboarding step as done for 'add job titles' to avoid display that screen again to the user: LcData.UserInfo.SetOnboardingStep(userID, "addJobTitles"); // Send email as provider LcMessaging.SendWelcomeProvider(userID, WebSecurity.CurrentUserName); } } else { throw new HttpException(404, "Job Title not found or disapproved"); } return(GetItem(userID, jobTitleID)); }
/// <summary> /// Process a request to create a user job title given a jobTitleID with /// a validated and sanitized jobTitleName (pass in GetValidatedJobTitleName result) /// as a custom listing title. /// </summary> /// <param name="userID"></param> /// <param name="jobTitleID"></param> /// <param name="jobTitleName"></param> /// <returns></returns> public dynamic Create(int userID, int jobTitleID, string jobTitleName) { if (jobTitleID == 0 || jobTitleID == LcRest.UserJobTitle.UserGeneratedJobTitleID) { // new-job-title version: it's possible that the user wrotes a // job title name without pick-up one from the list, on that case // the user generated job title is assigned and the given title name is // used as listing title // Name for the job title is required if (String.IsNullOrWhiteSpace(jobTitleName)) { throw new HttpException(400, "A Job Title is required"); } // Search: we try an exact match, just in case we have already the job title (singular or plural) and // user didn't select it from the list var locale = LcRest.Locale.Current; var jobTitle = LcRest.JobTitle.FindExactName(jobTitleName, locale.languageID, locale.countryID); if (jobTitle.HasValue) { // Use the first one jobTitleID = jobTitle.Value; } else { // Create a new job-title based on the given name #650 jobTitleID = LcRest.UserJobTitle.UserGeneratedJobTitleID; } } // Double check that the job title exists else { var existentTitle = LcRest.PublicJobTitle.Get(jobTitleID, LcRest.Locale.Current); if (existentTitle == null) { throw new HttpException(404, "Job Title not found or disapproved"); } // If exists, we use the user given title, with fallback to the one we have for the given jobTitleID else if (String.IsNullOrWhiteSpace(jobTitleName)) { jobTitleName = existentTitle.singularName; } } // Read data; It stops on not valid: var data = GetValidatedItemBodyInput(); LcRest.UserJobTitle.Create(new LcRest.UserJobTitle { userID = userID, jobTitleID = jobTitleID, title = jobTitleName, intro = data.intro, cancellationPolicyID = data.policyID, collectPaymentAtBookMeButton = data.collectPaymentAtBookMeButton, instantBooking = data.instantBooking }); // If user is just a client, needs to become a professional var user = LcRest.UserProfile.Get(userID); if (!user.isServiceProfessional) { LcAuth.BecomeProvider(userID); // Set onboarding step as done for 'add job title' to avoid display that screen again to the user: LcData.UserInfo.SetOnboardingStep(userID, "addJobTitle"); // Send email as provider LcMessaging.SendWelcomeProvider(userID, WebSecurity.CurrentUserName); } return(LcRest.UserJobTitle.GetItem(userID, jobTitleID)); }