// GET: /account/paypal-payment
        public ActionResult PaypalPayment(string tx = null)
        {
            PayPalPaymentResult result = PayPalPaymentResult.Canceled;

            var helper = new PayPalHelper();

            var txnId = !String.IsNullOrEmpty(tx)
                        // tx in PDT from Sandbox is upper-case on automatic return and lower-case on manual return. A PDT request returns error 4002 if given a low-case tx.
                ? tx.ToUpper()
                : "PDT " + KeyUtils.GetCurrentTimeKey();

            helper.WriteLog(PayPalLogEntity.NotificationKind.PDT, txnId, Request.Url.Query);

            if (!String.IsNullOrEmpty(tx))
            {
                // Query PayPal.
                var response = helper.RequestPaymentDetails(txnId);

                var logRowKey = helper.WriteLog(PayPalLogEntity.NotificationKind.DetailsRequest, txnId, response);

                // Parse the response
                var lines   = helper.SplitPdtMessage(response);
                var payment = helper.ParsePaymentLines(lines);

                // Write the payment to the database.
                if (helper.PostIncomingPayPalPayment(payment, logRowKey))
                {
                    result = PayPalPaymentResult.Success;
                }
            }

            return(RedirectToAction("Transactions", new { PayPalPaymentResult = result }));
        }
        public async Task <IHttpActionResult> PostResourceView([FromUri] int id, [FromBody] JObject value)
        {
            // A suggestion may have resourceId == null(0?) when the resource is made on the client in code and points to Google

            // TODO. Idea: Get the unauthenicated user's session cookie and log views for the session in a separate datastore.
            var userId = this.GetUserId(); // GetUserId() returns 0 for an unauthenticated user. That's fine. We log every view.

            var logEnity = new DynamicTableEntity(KeyUtils.GetCurrentTimeKey(), KeyUtils.IntToKey(userId));

            logEnity["Json"] = new EntityProperty(value.ToString());
            var logTask = AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.LibraryLog, logEnity);

            if (userId != 0 && id != 0)
            {
                var viewTask = DapperHelper.ExecuteResilientlyAsync("dbo.libPostResourceView",
                                                                    new
                {
                    UserId     = userId,
                    ResourceId = id,
                },
                                                                    CommandType.StoredProcedure);

                // We use KeyUtils.LocalTimeToInvertedKey() to keep the local time and to order last records first for retrieval.
                var localTime = (string)value["localTime"];
                var rowKey    = KeyUtils.LocalTimeToInvertedKey(localTime);

                if (String.IsNullOrEmpty(rowKey))
                {
                    throw new ArgumentOutOfRangeException(localTime);
                }

                var historyEntity = new LibraryHistoryEntity
                {
                    PartitionKey = KeyUtils.IntToKey(userId),
                    RowKey       = rowKey,
                    ResourceId   = id,
                };
                var historyTask = AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.LibraryHistory, historyEntity);

                // Do all the tasks in parallel.
                await Task.WhenAll(viewTask, historyTask, logTask);
            }
            else
            {
                await logTask;
            }

            return(StatusCode(HttpStatusCode.NoContent));
        }
Exemple #3
0
        public async Task <IHttpActionResult> InboundWebhook(InboundWebhookFormModel form)
        {
            // The form is empty on a HEAD request
            if (form != null)
            {
                var mandrill_events = form.mandrill_events;
                var partitionKey    = KeyUtils.GetCurrentTimeKey();

                var entity = new ExternalSessionLogEntity
                {
                    PartitionKey = partitionKey,
                    RowKey       = "InboundWebhook",
                    Data         = mandrill_events,
                };
                await AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.ExternalSessions, entity);

                await SendSmsMessage("InboundWebhook " + partitionKey, partitionKey);
            }

            return(StatusCode(HttpStatusCode.NoContent));
        }
Exemple #4
0
        public string WriteLog(PayPalLogEntity.NotificationKind kind, string txnId, string logData)
        {
            if (String.IsNullOrEmpty(txnId))
            {
                throw new ArgumentException();
            }

            var rowKey = KeyUtils.GetCurrentTimeKey();

            var entity = new PayPalLogEntity
            {
                PartitionKey = txnId,
                RowKey       = rowKey,
                Kind         = kind.ToString(),
                LogData      = logData,
            };

            AzureStorageUtils.InsertEntity(AzureStorageUtils.TableNames.PaymentLog, entity);

            return(rowKey);
        }
Exemple #5
0
        // Extract text from the HTML page
        private string FindText(string html, string startSnippet, string endSnippet)
        {
            var i1 = html.IndexOf(startSnippet);
            var i2 = html.IndexOf(endSnippet, i1 + 1);

            if ((i1 == -1) || (i2 == -1))
            {
                // Write the poison HTML page to the log.
                var partitionKey = KeyUtils.GetCurrentTimeKey();
                var entity       = new ExternalSessionLogEntity
                {
                    PartitionKey = partitionKey,
                    RowKey       = "Error_FindText",
                    Data         = html,
                };
                AzureStorageUtils.InsertEntity(AzureStorageUtils.TableNames.ExternalSessions, entity);

                throw new FormatException("Parsing the external service data. Snippet not found. " + startSnippet.Length.ToString() + " " + endSnippet.Length.ToString());
            }
            var shift = startSnippet.Length;

            return(html.Substring(i1 + shift, i2 - i1 - shift));
        }
Exemple #6
0
        public async Task <IHttpActionResult> PostBooking([FromBody] JObject json)
        {
            TimeUtils.ThrowIfClientTimeIsAmbiguous((string)json["localTime"], (int)json["localTimezoneOffset"]);

            var teacherUserId = (int)json["teacherUserId"];
            var start         = (DateTime)json["start"];
            var end           = (DateTime)json["end"];
            var price         = (decimal)json["price"];

            var userId = this.GetUserId();

            var teacher = SessionHelper.ItalkiTeachers.First(i => i.UserId == teacherUserId);

            var periods = await SessionHelper.GetTeacherPeriods(teacherUserId, true);

            var available = periods.Any(i => (i.Start <= start) && (i.End >= end));

            if (!available)
            {
                throw new Exception(ItalkiHelper.TimeSlotUnavailableError);
            }

            /* Compensation is the mechanism by which previously completed work can be undone or compensated when a subsequent failure occurs.
             * The Compensation pattern is required in error situations when multiple atomic operations cannot be linked with classic transactions.
             */

            // We cannot use a transaction in a heterogeneous storage mediums. An orphaned message on Azure Table is not a problem, we do not compensate it on error. The main message metadata is in the database anyway.
            var message      = (string)json["message"];
            var messageExtId = await SessionHelper.SaveMessageAndGetExtId(message);

            var sessionId = (await DapperHelper.QueryResilientlyAsync <int>("dbo.sesBookSession", new
            {
                UserId = userId, // who requests and pays
                Start = start,
                End = end,
                TeacherUserId = teacherUserId,
                Price = price,
                MessageExtId = messageExtId,
            },
                                                                            CommandType.StoredProcedure))
                            .Single();

            // We will write to the log.
            var partitionKey = KeyUtils.GetCurrentTimeKey();

            // Book the session on Italki.
            try
            {
                long?sessionExtId = null;
#if DEBUG
                sessionExtId = 0;
#else
                var helper = new ItalkiHelper();
                sessionExtId = await helper.BookSession(teacher.CourseId, start, teacher.ScheduleUrl, partitionKey);
#endif
                if (sessionExtId.HasValue)
                {
                    await DapperHelper.ExecuteResilientlyAsync("dbo.sesUpdateSessionExtId", new
                    {
                        SessionId = sessionId,
                        ExtId     = sessionExtId,
                    },
                                                               CommandType.StoredProcedure);
                }
            }
            catch (Exception ex)
            {
                // Compensation on booking failure.
                // Clean up the session.
                DapperHelper.ExecuteResiliently("dbo.sesCancelSession", new
                {
                    UserId     = userId,
                    SessionId  = sessionId,
                    ClearUsers = true,
                },
                                                CommandType.StoredProcedure);

                throw new UserAlertException("Failed to book session", ex);
            }

            HostingEnvironment.QueueBackgroundWorkItem(async(ct) => await SendSmsMessage("Session request. Teacher " + teacherUserId.ToString(), partitionKey));

            return(StatusCode(HttpStatusCode.NoContent));
        }
        public ActionResult PaypalIpn()
        {
            var content = Request.BinaryRead(Request.ContentLength);
            var message = Encoding.ASCII.GetString(content);

            var helper = new PayPalHelper();

            var lines   = helper.SplitIpnMessage(message);
            var payment = helper.ParsePaymentLines(lines);
            var txnId   = !String.IsNullOrEmpty(payment.TxnId) ? payment.TxnId : "IPN " + KeyUtils.GetCurrentTimeKey();

            var logRowKey = helper.WriteLog(PayPalLogEntity.NotificationKind.IPN, txnId, message);

            var response = helper.VerifyIPN(message, txnId);

            helper.WriteLog(PayPalLogEntity.NotificationKind.IPNResponse, txnId, response);

            if (response == "VERIFIED")
            {
                if (helper.PostIncomingPayPalPayment(payment, logRowKey))
                {
                    //helper.PurchaseOnIncomingPayPalPayment(payment);
                }
            }

            return(new HttpStatusCodeResult(HttpStatusCode.NoContent));
        }
Exemple #8
0
        public async Task <long?> BookSession(int courseId, DateTime start, string referer, string tablePartitionKey)
        {
            // Time slot is encoded in a weired way.
            var origin = DateTime.UtcNow.Date;
            var diff   = start - origin;
            var day    = Convert.ToInt32(Math.Floor(diff.TotalDays)) - 1; // Day number for the next day is 0.

            if (day < 0)
            {
                throw new Exception(ItalkiHelper.TimeSlotUnavailableError);
            }
            var halfHour = (diff.Hours * 2) + (diff.Minutes == 30 ? 1 : 0); // Convert.ToInt32(1.0 * diff.Minutes / 30); // We expect minutes to be either 00 or 30.
            var timeText = String.Format("{0},{1}", day, halfHour);

            // We will write to the log.
            var partitionKey0 = KeyUtils.GetCurrentTimeKey();

            // Pre-booking validation. It might be redundand. We may want to do it just in case.
            // Note that the validation call does not send the courseId. That means the server uses a session(???) or a cookie or the Referer header. It may cause problems in a multi-teacher scenario, until we use a separate HttpClient for each teacher.
            //var content1 = new FormUrlEncodedContent(new[]
            //{
            //    new KeyValuePair<string, string>("MethodName", "ValidSessionTime"),
            //    new KeyValuePair<string, string>("time", timeText),
            //});
            //var request1 = new HttpRequestMessage
            //{
            //    RequestUri = new Uri(ItalkiWebServicesUrl),
            //    Method = HttpMethod.Post,
            //    Content = content1,
            //};
            //request1.Headers.Add("Referer", referer);

            //var response1 = await httpClient.SendAsync(request1);

            //var statusCode1 = (int)response1.StatusCode;
            //string responseContent1 = null;
            //if (response1.IsSuccessStatusCode)
            //{
            //    responseContent1 = await response1.Content.ReadAsStringAsync();
            //}

            //var entity1 = new ExternalSessionLogEntity
            //{
            //    PartitionKey = partitionKey,
            //    RowKey = "ValidationResponse",
            //    Data = responseContent1,
            //    HttpStatus = statusCode1,
            //};
            //await AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.ExternalSessions, entity1);

            //response1.EnsureSuccessStatusCode();

            // Send the booking request
#if DEBUG
            var password = "******";
#else
            var password = ConfigurationManager.AppSettings["Italki.Password1"];
#endif
            var content = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair <string, string>("MethodName", "RequestSession"),
                new KeyValuePair <string, string>("time", timeText), // It will be Url-encoded by the form oject.
                new KeyValuePair <string, string>("tool", "1"),
                new KeyValuePair <string, string>("sim", "andrey.fomin3"),
                new KeyValuePair <string, string>("msg", ":)"), // ":-)  :)  :D  :o)  :]  :3  :c)  :>  =]  8)  =)  :}  :^)"
                new KeyValuePair <string, string>("pwd", password),
                new KeyValuePair <string, string>("cid", courseId.ToString()),
            });

            var request = new HttpRequestMessage
            {
                RequestUri = new Uri(ItalkiWebServicesUrl),
                Method     = HttpMethod.Post,
                Content    = content,
            };
            request.Headers.Add("Referer", referer);

            // Write to the log
            var entity = new ExternalSessionLogEntity
            {
                PartitionKey = tablePartitionKey,
                RowKey       = "ItalkiRequest",
                Data         = JsonUtils.SerializeAsJson(new
                {
                    CourseId = courseId,
                    Start    = start,
                    Time     = timeText,
                }),
            };
            await AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.ExternalSessions, entity);

            // Book the session with Italki
            var response = await httpClient.SendAsync(request);

            var    statusCode      = (int)response.StatusCode;
            string responseContent = null;
            if (response.IsSuccessStatusCode)
            {
                responseContent = await response.Content.ReadAsStringAsync();
            }

            // Write the response to the log
            entity = new ExternalSessionLogEntity
            {
                PartitionKey = tablePartitionKey,
                RowKey       = "ItalkiResponse",
                Data         = responseContent,
                HttpStatus   = statusCode,
            };
            await AzureStorageUtils.InsertEntityAsync(AzureStorageUtils.TableNames.ExternalSessions, entity);

            response.EnsureSuccessStatusCode();

            long?extSessionId = null;
            try
            {
                var items = JArray.Parse(responseContent);

                /* +https://secure.italki.com/sessions/schedule_20131106.js?v=150302 , Line 853
                 * if(result[0]=="-2") $get("MsgPrompt").innerHTML=TS312;
                 * if(result[0]=="-1") $get("MsgPrompt").innerHTML=TS313;
                 * if(result[0]=="1") $get("MsgPrompt").innerHTML=TS314;
                 * if(result[0]=="-3") $get("MsgPrompt").innerHTML=TS315;
                 * In HTML
                 * var TS312="The time you\'re scheduling was taken by other students. Please try other time.";
                 * var TS313="Your payment account balance is not enough to pay for the session(s) you are scheduling.";
                 * var TS314="Trial session is not available.";
                 * var TS315="Incorrect password";
                 */
                var result = (string)items.FirstOrDefault();
                if ((result == "Y") || (result == "Y1"))
                {
                    var idArr = items[3];
                    if (idArr is JArray)
                    {
                        extSessionId = (long)idArr.First();
                    }
                }
            }
            // We may get an HTML instead of JSON in response.
            catch (JsonReaderException)
            {
            }

            return(extSessionId);
        }