├── src ├── NubankCli │ ├── .gitignore │ ├── Models │ │ ├── NuSession.cs │ │ └── NuAppSettings.cs │ ├── Commands │ │ ├── Balances │ │ │ ├── EntityNames.cs │ │ │ └── GetCommand.cs │ │ ├── Statements │ │ │ ├── EntityNames.cs │ │ │ ├── StatementTableConfig.cs │ │ │ └── GetCommand.cs │ │ ├── Transactions │ │ │ ├── EntityNames.cs │ │ │ ├── TransactionTableConfig.cs │ │ │ └── GetCommand.cs │ │ ├── Main │ │ │ └── MainCommand.cs │ │ ├── Auth │ │ │ ├── WhoamiCommand.cs │ │ │ ├── LogoutCommand.cs │ │ │ └── AuthCommand.cs │ │ └── Imports │ │ │ ├── ImportDebitCommand.cs │ │ │ └── ImportCreditCommand.cs │ ├── Extensions │ │ ├── Formattters │ │ │ ├── FormatEnumExtensions.cs │ │ │ ├── FormatGuidExtensions.cs │ │ │ ├── FormatBooleanExtensions.cs │ │ │ ├── FormatNumbersExtensions.cs │ │ │ ├── FormatConditional.cs │ │ │ ├── FormatDateTimeExtensions.cs │ │ │ ├── FormatObjectExtensions.cs │ │ │ └── FormatStringExtensions.cs │ │ ├── Tables │ │ │ ├── ITableConfig.cs │ │ │ ├── TableColumn.cs │ │ │ └── TablesExtensions.cs │ │ ├── Langs │ │ │ └── MessagesPtBr.cs │ │ ├── ItemCollectionExtensions.cs │ │ ├── Configurations │ │ │ └── ConfigurationExtensions.cs │ │ ├── Environments │ │ │ └── EnvironmentExtensions.cs │ │ └── CommandExtensions.cs │ ├── settings.json │ ├── sbin │ │ └── publish.sh │ ├── NubankCli.csproj │ └── Program.cs ├── NubankSharp │ ├── Repositories │ │ ├── Api │ │ │ ├── NuApi.cs │ │ │ ├── NuAuthApi.cs │ │ │ ├── Model │ │ │ │ ├── Link.cs │ │ │ │ ├── SelfLink.cs │ │ │ │ ├── Bills │ │ │ │ │ ├── BillState.cs │ │ │ │ │ ├── GetBillsResponse.cs │ │ │ │ │ ├── BillSummary.cs │ │ │ │ │ └── GetBillResponse.cs │ │ │ │ ├── Events │ │ │ │ │ ├── GetEventsResponse.cs │ │ │ │ │ ├── Event.cs │ │ │ │ │ └── EventCategory.cs │ │ │ │ ├── Login │ │ │ │ │ ├── ConnectAppResponse.cs │ │ │ │ │ └── LoginResponse.cs │ │ │ │ ├── Savings │ │ │ │ │ ├── GetSavingsAccountFeedResponse.cs │ │ │ │ │ └── SavingFeed.cs │ │ │ │ └── Balance │ │ │ │ │ └── GetBalanceResponse.cs │ │ │ ├── IGqlQueryRepository.cs │ │ │ ├── Queries │ │ │ │ ├── balance.gql │ │ │ │ └── savings.gql │ │ │ ├── NuHttpClientLogging.cs │ │ │ ├── GqlQueryRepository.cs │ │ │ ├── EndPointApi.cs │ │ │ ├── Converters │ │ │ │ └── TolerantEnumConverter.cs │ │ │ ├── Services │ │ │ │ └── BillService.cs │ │ │ └── NuHttpClient.cs │ │ └── Files │ │ │ ├── JsonFileRepository.cs │ │ │ └── StatementFileRepository.cs │ ├── Entities │ │ ├── StatementType.cs │ │ ├── CardType.cs │ │ ├── Statement.cs │ │ ├── TransactionType.cs │ │ ├── Card.cs │ │ ├── NuUser.cs │ │ └── Transaction.cs │ ├── Exceptions │ │ └── UnauthorizedException.cs │ ├── Constants.cs │ ├── DTOs │ │ └── SummaryDTO.cs │ ├── Extensions │ │ ├── FileExtensions.cs │ │ ├── DecimalExtensions.cs │ │ ├── DateTimeExtensions.cs │ │ ├── StringExtensions.cs │ │ ├── JwtDecoderExtensions.cs │ │ ├── TransactionExtensions.cs │ │ └── StatementExtensions.cs │ └── NubankSharp.csproj └── NuBankCli.sln ├── nu ├── wiremock ├── __files │ ├── Login.json │ ├── discovery.json │ ├── events.json │ ├── 2019-01-17_2019-02-17.json │ ├── 2019-02-17_2019-03-17.json │ ├── bills.json │ ├── nuconta.json │ ├── app-discovery.json │ └── LoginAccessTokenTwoFactory.json.json └── mappings │ ├── nuconta.json │ ├── bills.json │ ├── events.json │ ├── 2019-01-17_2019-02-17.json │ ├── 2019-02-17_2019-03-17.json │ ├── app-discovery.json │ ├── discovery.json │ ├── Login.json │ └── LoginAccessTokenTwoFactory.json.json ├── LICENSE ├── .vscode ├── launch.json └── tasks.json ├── .gitignore └── README.md /src/NubankCli/.gitignore: -------------------------------------------------------------------------------- 1 | Logs/ 2 | UsersData/ 3 | Queries/ -------------------------------------------------------------------------------- /nu: -------------------------------------------------------------------------------- 1 | DIR="$(dirname "${BASH_SOURCE[0]}")" 2 | dotnet run --project "$DIR/src/NubankCli/NubankCli.csproj" "$@" -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/NuApi.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorgasparotto/NubankCli/HEAD/src/NubankSharp/Repositories/Api/NuApi.cs -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/NuAuthApi.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/juniorgasparotto/NubankCli/HEAD/src/NubankSharp/Repositories/Api/NuAuthApi.cs -------------------------------------------------------------------------------- /src/NubankSharp/Entities/StatementType.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp 2 | { 3 | public enum StatementType 4 | { 5 | ByMonth, 6 | ByBill 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/NubankSharp/Entities/CardType.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Entities 2 | { 3 | public enum CardType 4 | { 5 | NuConta, 6 | CreditCard 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/NubankCli/Models/NuSession.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Models 2 | { 3 | public class NuSession 4 | { 5 | public string CurrentUser { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/NubankCli/Commands/Balances/EntityNames.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Commands.Balances 2 | { 3 | public enum EntityNames 4 | { 5 | Balance, 6 | Total 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Link.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Repositories.Api 2 | { 3 | public class Link 4 | { 5 | public string Href { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/NubankSharp/Exceptions/UnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Exceptions 4 | { 5 | public class UnauthorizedException : Exception 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/SelfLink.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Repositories.Api 2 | { 3 | public class SelfLink 4 | { 5 | public Link Self { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatEnumExtensions.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace NubankSharp.Cli.Extensions.Formatters 3 | { 4 | public static class FormatEnumExtensions 5 | { 6 | 7 | } 8 | } -------------------------------------------------------------------------------- /src/NubankCli/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "NubankUrl": "https://prod-s0-webapp-proxy.nubank.com.br", 3 | "MockUrl": "http://localhost:6513", 4 | "EnableMockServer": false, 5 | "EnableDebugFile": true 6 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/IGqlQueryRepository.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Repositories.Api 2 | { 3 | public interface IGqlQueryRepository 4 | { 5 | string GetGql(string queryName); 6 | } 7 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Queries/balance.gql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | savingsAccount { 4 | currentSavingsBalance { 5 | netAmount 6 | } 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Statements/EntityNames.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Commands.Statements 2 | { 3 | public enum EntityNames 4 | { 5 | Stat, 6 | Statement, 7 | Statements, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/NubankCli/Commands/Transactions/EntityNames.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Commands.Transactions 2 | { 3 | public enum EntityNames 4 | { 5 | Trans, 6 | Transaction, 7 | Transactions, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Bills/BillState.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Repositories.Api 2 | { 3 | public enum BillState 4 | { 5 | Future, 6 | Open, 7 | Closed, 8 | Overdue 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Events/GetEventsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NubankSharp.Repositories.Api 4 | { 5 | public class GetEventsResponse 6 | { 7 | public List Events { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Tables/ITableConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NubankSharp.Extensions.Tables 4 | { 5 | public interface ITableConfig 6 | { 7 | IEnumerable> GetTableColumns(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/NubankCli/sbin/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #dotnet publish --output "./out" --runtime win-x64 --configuration Release \ 4 | #-p:PublishSingleFile=true -p:PublishTrimmed=true --self-contained true 5 | 6 | dotnet publish --output "./out" -r win-x64 -p:PublishSingleFile=true --self-contained false -c Release -------------------------------------------------------------------------------- /src/NubankSharp/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp 4 | { 5 | public class Constants 6 | { 7 | public const int BANK_ID = 260; 8 | public static TimeZoneInfo BR_TIME_ZONE = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatGuidExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Cli.Extensions.Formatters 4 | { 5 | public static class FormatGuidExtensions 6 | { 7 | public static string HumanizeDefault(this Guid guid) 8 | { 9 | var split = guid.ToString().Split('-'); 10 | return split[0]; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/NubankCli/Models/NuAppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Models 2 | { 3 | public class NuAppSettings 4 | { 5 | public string NubankUrl { get; set; } 6 | public string MockUrl { get; set; } 7 | public bool EnableMockServer { get; set; } 8 | public bool EnableDebugFile { get; set; } 9 | public string CurrentUser { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Langs/MessagesPtBr.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Extensions.Langs 2 | { 3 | public class MessagesPtBr 4 | { 5 | public const string SEARCH_EMPTY_RESULT = "Nenhum resultado encontrado"; 6 | public const string SEARCH_RESULT = "Encontrado {0} registros"; 7 | public const string SEARCH_PAGINATION_RESULT = "Página {0}/{1} de um total de {2} linhas"; 8 | } 9 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Main/MainCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using SysCommand.ConsoleApp; 3 | using SysCommand.ConsoleApp.Results; 4 | 5 | namespace NubankSharp.Cli 6 | { 7 | public class MainCommand : Command 8 | { 9 | public RedirectResult Main(string[] args = null) 10 | { 11 | return new RedirectResult("help"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Tables/TableColumn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Extensions.Tables 4 | { 5 | public class TableColumn 6 | { 7 | public string Name { get; set; } 8 | public Func ValueFormatter { get; set; } 9 | public Func ValueFormatterWide { get; set; } 10 | public bool OnlyInWide { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/NubankSharp/DTOs/SummaryDTO.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.DTOs 2 | { 3 | public class SummaryDTO 4 | { 5 | public int CountIn { get; set; } 6 | public decimal ValueIn { get; set; } 7 | 8 | public int CountOut { get; set; } 9 | public decimal ValueOut { get; set; } 10 | 11 | public int CountTotal { get; set; } 12 | public decimal ValueTotal { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Bills/GetBillsResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NubankSharp.Repositories.Api 4 | { 5 | public class GetBillsResponse 6 | { 7 | public List Bills { get; set; } 8 | public BillsLinks Links { get; set; } 9 | } 10 | 11 | public class BillsLinks 12 | { 13 | public Link Open { get; set; } 14 | public Link Future { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/NuHttpClientLogging.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Repositories.Api 4 | { 5 | public class NuHttpClientLogging 6 | { 7 | public string Folder { get; } 8 | public string UserName { get; } 9 | public string Scope { get; } 10 | 11 | public NuHttpClientLogging(string userName, string scope, string folder = null) 12 | { 13 | this.Folder = folder ?? "Logs"; 14 | this.UserName = userName; 15 | this.Scope = $"{scope}-{DateTime.Now:yyyyMMddHHmmss}"; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatBooleanExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Cli.Extensions.Formatters 2 | { 3 | public static class FormatBooleanExtensions 4 | { 5 | public static string HumanizeDefault(this bool? value) 6 | { 7 | if (value == null) 8 | return "-"; 9 | 10 | return HumanizeDefault(value.Value); 11 | } 12 | 13 | public static string HumanizeDefault(this bool value) 14 | { 15 | if (value) 16 | return "Sim"; 17 | 18 | return "Não"; 19 | } 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /src/NubankCli/Extensions/ItemCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using SysCommand.ConsoleApp; 3 | 4 | namespace NubankSharp.Extensions 5 | { 6 | public static class ItemCollectionExtensions 7 | { 8 | public static void SetServiceProvider(this ItemCollection itemCollection, IServiceProvider provider) 9 | { 10 | itemCollection[nameof(IServiceProvider)] = provider; 11 | } 12 | 13 | public static IServiceProvider GetServiceProvider(this ItemCollection itemCollection) 14 | { 15 | return (IServiceProvider)itemCollection[nameof(IServiceProvider)]; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatNumbersExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Cli.Extensions.Formatters 4 | { 5 | public static class FormatNumbersExtensions 6 | { 7 | public static string HumanizeDefault(this decimal value) 8 | { 9 | return string.Format("{0:C2}", value); 10 | } 11 | 12 | public static string HumanizeDefault(this double value) 13 | { 14 | return string.Format("{0:C2}", value); 15 | } 16 | 17 | public static string HumanizeDefault(this float value) 18 | { 19 | return string.Format("{0:C2}", value); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/NubankSharp/Entities/Statement.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | 6 | namespace NubankSharp.Entities 7 | { 8 | [DebuggerDisplay("Start: {Start} End: {End} Count: {Transactions.Count}")] 9 | public class Statement 10 | { 11 | public const string Version1_0 = "1.0.0"; 12 | public string Version { get; set; } 13 | public DateTime Start { get; set; } 14 | public DateTime End { get; set; } 15 | public Card Card { get; set; } 16 | public StatementType StatementType { get; set; } 17 | public List Transactions { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/FileExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace NubankSharp.Extensions 4 | { 5 | public static class FileExtensions 6 | { 7 | /// 8 | /// Create the folder if not existing for a full file name 9 | /// 10 | /// full path of the file 11 | public static void CreateFolderIfNeeded(string filename) 12 | { 13 | string folder = Path.GetDirectoryName(filename); 14 | if (!string.IsNullOrEmpty(folder) && !Directory.Exists(folder)) 15 | { 16 | Directory.CreateDirectory(folder); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Tables/TablesExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using NubankSharp.Commands.Statements; 3 | using NubankSharp.Commands.Transactions; 4 | using NubankSharp.Extensions.Tables; 5 | using NubankSharp.Entities; 6 | 7 | namespace NubankSharp.Extensions.Tables 8 | { 9 | public static class TablesExtensions 10 | { 11 | public static IServiceCollection ConfigureTables(this IServiceCollection service) 12 | { 13 | service.AddSingleton>(s => new TransactionTableConfig()); 14 | service.AddSingleton>(s => new StatementTableConfig()); 15 | 16 | return service; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Login/ConnectAppResponse.cs: -------------------------------------------------------------------------------- 1 | using QRCoder; 2 | using System.Drawing; 3 | using System.Security.Cryptography; 4 | 5 | namespace NubankSharp.Repositories.Api 6 | { 7 | public class ConnectAppResponse 8 | { 9 | public string UserName { get; set; } 10 | public string Password { get; set; } 11 | public string PrivateKey { get; set; } 12 | public string PrivateKeyCrypto { get; set; } 13 | public string PublicKey { get; set; } 14 | public string PublicKeyCrypto { get; set; } 15 | public string Model { get; set; } 16 | public string DeviceId { get; set; } 17 | public string EncryptedCode { get; set; } 18 | public string SentTo { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/DecimalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace NubankSharp.Extensions 4 | { 5 | public static class DecimalExtensions 6 | { 7 | public static decimal? ParseFromPtBr(string valueStr, bool throwException = true) 8 | { 9 | valueStr = valueStr.Replace("R$", "").Trim(); 10 | var myCI = new CultureInfo("pt-BR", false).Clone() as CultureInfo; 11 | myCI.NumberFormat.CurrencyDecimalSeparator = ","; 12 | 13 | if (throwException) 14 | return decimal.Parse(valueStr, NumberStyles.Any, myCI); 15 | else if (decimal.TryParse(valueStr, NumberStyles.Any, myCI, out var ret)) 16 | return ret; 17 | 18 | return null; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Extensions 4 | { 5 | public static class DateTimeExtensions 6 | { 7 | /// 8 | /// Retorna a data com o mês no primeiro dia 9 | /// In : 2020-01-20 10 | /// Out: 2020-01-01 11 | /// 12 | /// 13 | /// 14 | public static DateTime GetDateBeginningOfMonth(this DateTime date) 15 | { 16 | return new DateTime(date.Year, date.Month, 1); 17 | } 18 | 19 | public static DateTime GetDateEndOfMonth(this DateTime date) 20 | { 21 | return new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Configurations/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using NubankSharp.Models; 4 | using System.IO; 5 | 6 | namespace NubankCli.Extensions.Configurations 7 | { 8 | public static class ConfigurationExtensions 9 | { 10 | public static IServiceCollection AddConfigurations(this IServiceCollection services, string configFolder, string settingsFileName) 11 | { 12 | var builder = new ConfigurationBuilder().AddJsonFile(Path.Combine(configFolder, settingsFileName)); 13 | 14 | var config = builder.Build(); 15 | services.AddScoped((s) => config); 16 | 17 | services.Configure(options => config.Bind(options)); 18 | return services; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NubankSharp/Entities/TransactionType.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Entities 2 | { 3 | public enum TransactionType 4 | { 5 | Unknown, 6 | WelcomeEvent, 7 | TransferInEvent, 8 | TransferOutEvent, 9 | BarcodePaymentEvent, 10 | BarcodePaymentFailureEvent, 11 | CanceledScheduledTransferOutEvent, 12 | AddToReserveEvent, 13 | DebitPurchaseEvent, 14 | DebitPurchaseReversalEvent, 15 | BillPaymentEvent, 16 | CanceledScheduledBarcodePaymentRequestEvent, 17 | RemoveFromReserveEvent, 18 | TransferOutReversalEvent, 19 | SalaryPortabilityRequestEvent, 20 | SalaryPortabilityRequestApprovalEvent, 21 | DebitWithdrawalFeeEvent, 22 | DebitWithdrawalEvent, 23 | CreditEvent, 24 | GenericFeedEvent, 25 | LendingTransferInEvent, 26 | LendingTransferOutEvent 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NubankCli/Commands/Auth/WhoamiCommand.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp.Entities; 2 | using NubankSharp.Extensions; 3 | using NubankSharp.Repositories.Files; 4 | using SysCommand.ConsoleApp; 5 | using System; 6 | 7 | namespace NubankSharp.Cli 8 | { 9 | public class WhoamiCommand : Command 10 | { 11 | public void Whoami() 12 | { 13 | try 14 | { 15 | var user = this.GetCurrentUser(); 16 | App.Console.Write($"USERNAME : {user.UserName}"); 17 | App.Console.Write($"LOCALIZAÇÃO USUÁRIO: {this.GetUserFileName(user)}"); 18 | App.Console.Write($" "); 19 | App.Console.Warning($"Seu token gerado por '{user.GetLoginType()}' expira em: {user.GetExpiredDate()}"); 20 | } 21 | catch (Exception ex) 22 | { 23 | this.ShowApiException(ex); 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Events/Event.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace NubankSharp.Repositories.Api 5 | { 6 | public class Event 7 | { 8 | public string Description { get; set; } 9 | [JsonConverter(typeof(TolerantEnumConverter))] 10 | public EventCategory Category { get; set; } 11 | public decimal Amount { get; set; } 12 | public decimal CurrencyAmount => Amount / 100; 13 | public DateTimeOffset Time { get; set; } 14 | public string Title { get; set; } 15 | public string Message { get; set; } 16 | public string Href { get; set; } 17 | public EventDetails Details { get; set; } 18 | 19 | [JsonProperty("_links")] 20 | public SelfLink Links { get; set; } 21 | } 22 | 23 | public class EventDetails 24 | { 25 | public decimal Lat { get; set; } 26 | public decimal Lon { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /wiremock/__files/Login.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.4Adcj3UFYzPUVaVF43FmMab6RlaQD8A9V8wFzzht-KQ", 3 | "token_type": "bearer", 4 | "_links": { 5 | "revoke_token": { 6 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revoke_token" 7 | }, 8 | "revoke_all": { 9 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revoke_all" 10 | }, 11 | "account_emergency": { 12 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/account_emergency" 13 | }, 14 | "bill_emergency": { 15 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/bill_emergency" 16 | } 17 | }, 18 | "refresh_token": "string token", 19 | "refresh_before": "2020-11-05T02:39:35Z" 20 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Balances/GetCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Commands.Balances 2 | { 3 | using SysCommand.ConsoleApp; 4 | using SysCommand.Mapping; 5 | using System; 6 | using NubankSharp.Extensions; 7 | 8 | public partial class BalanceCommand : Command 9 | { 10 | 11 | [Action(Name = "get")] 12 | public void GetBalance( 13 | EntityNames type, 14 | [Argument(ShortName = 'o', LongName = "output")] string outputFormat = null 15 | ) 16 | { 17 | try 18 | { 19 | var user = this.GetCurrentUser(); 20 | var nuApi = this.CreateNuApiByUser(user, nameof(GetBalance)); 21 | var total = nuApi.GetBalance(); 22 | this.ViewSingleFormatted(new { Total = total }, outputFormat); 23 | } 24 | catch (Exception ex) 25 | { 26 | this.ShowApiException(ex); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/NubankCli/Commands/Auth/LogoutCommand.cs: -------------------------------------------------------------------------------- 1 | using SysCommand.ConsoleApp; 2 | using System; 3 | using NubankSharp.Extensions; 4 | using NubankSharp.Repositories.Files; 5 | using NubankSharp.Entities; 6 | 7 | namespace NubankSharp.Cli 8 | { 9 | public class LogoutCommand : Command 10 | { 11 | public void Logout() 12 | { 13 | try 14 | { 15 | var user = this.GetCurrentUser(); 16 | user.Token = null; 17 | user.RefreshToken = null; 18 | user.CertificateBase64 = null; 19 | user.CertificateCryptoBase64 = null; 20 | user.AutenticatedUrls = null; 21 | 22 | this.SaveUser(user); 23 | this.SetCurrentUser(null); 24 | 25 | App.Console.Success("Logout efetuado com sucesso!"); 26 | } 27 | catch (Exception ex) 28 | { 29 | this.ShowApiException(ex); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.DTOs; 3 | using NubankSharp.Entities; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Web; 9 | 10 | namespace NubankSharp.Extensions 11 | { 12 | public static class StringExtensions 13 | { 14 | public static string GetFileNameFromUrl(string url) 15 | { 16 | var decoded = HttpUtility.UrlDecode(url); 17 | 18 | if (decoded.IndexOf("?") is { } queryIndex && queryIndex != -1) 19 | { 20 | decoded = decoded.Substring(0, queryIndex); 21 | } 22 | 23 | return Path.GetFileName(decoded); 24 | } 25 | 26 | public static string BeautifyJson(string str) 27 | { 28 | var obj = JsonConvert.DeserializeObject(str); 29 | string json = JsonConvert.SerializeObject(obj, Formatting.Indented); 30 | return json; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NubankCli/NubankCli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net5 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Always 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /wiremock/mappings/nuconta.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2cf63a7f-de43-3acf-83b2-e2aa0b08d57a", 3 | "request": { 4 | "url": "/api/proxy/nuconta", 5 | "method": "POST" 6 | }, 7 | "response": { 8 | "status": 200, 9 | "bodyFileName": "nuconta.json", 10 | "headers": { 11 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 12 | "Content-Type": "application/json;charset=utf-8", 13 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 14 | "X-Content-Type-Options": "nosniff", 15 | "X-Download-Options": "noopen", 16 | "x-frame-options": "DENY", 17 | "x-http2-stream-id": "3", 18 | "X-Permitted-Cross-Domain-Policies": "none", 19 | "x-xss-protection": "1; mode=block", 20 | "transfer-encoding": "chunked", 21 | "Connection": "keep-alive" 22 | } 23 | }, 24 | "uuid": "2cf63a7f-de43-3acf-83b2-e2aa0b08d57a" 25 | } -------------------------------------------------------------------------------- /wiremock/mappings/bills.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3d8499fc-b498-314f-9cd6-c0ac720ee425", 3 | "request": { 4 | "url": "/api/proxy/bills_summary", 5 | "method": "GET" 6 | }, 7 | "response": { 8 | "status": 200, 9 | "bodyFileName": "bills.json", 10 | "headers": { 11 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 12 | "Content-Type": "application/json;charset=utf-8", 13 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 14 | "Vary": "Accept-Encoding, User-Agent", 15 | "X-Content-Type-Options": "nosniff", 16 | "X-Download-Options": "noopen", 17 | "x-frame-options": "DENY", 18 | "x-http2-stream-id": "7", 19 | "X-Permitted-Cross-Domain-Policies": "none", 20 | "x-xss-protection": "1; mode=block", 21 | "Connection": "keep-alive" 22 | } 23 | }, 24 | "uuid": "3d8499fc-b498-314f-9cd6-c0ac720ee425" 25 | } -------------------------------------------------------------------------------- /wiremock/mappings/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "82a4eb13-d0f1-3559-8010-c4550312a1cf", 3 | "request": { 4 | "url": "/api/proxy/events", 5 | "method": "GET" 6 | }, 7 | "response": { 8 | "status": 200, 9 | "bodyFileName": "events.json", 10 | "headers": { 11 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 12 | "Content-Type": "application/json;charset=utf-8", 13 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 14 | "Vary": "Accept-Encoding, User-Agent", 15 | "X-Content-Type-Options": "nosniff", 16 | "X-Download-Options": "noopen", 17 | "x-frame-options": "DENY", 18 | "x-http2-stream-id": "45", 19 | "X-Permitted-Cross-Domain-Policies": "none", 20 | "x-xss-protection": "1; mode=block", 21 | "transfer-encoding": "chunked", 22 | "Connection": "keep-alive" 23 | } 24 | }, 25 | "uuid": "82a4eb13-d0f1-3559-8010-c4550312a1cf" 26 | } -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatConditional.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NubankSharp.Cli.Extensions.Formatters 4 | { 5 | public interface IFormatConditionalBase 6 | { 7 | Func CanFormat { get; set; } 8 | string FormatValue(object obj); 9 | } 10 | 11 | public class FormatConditional : IFormatConditionalBase 12 | { 13 | private Func _canFormat; 14 | 15 | public Func CanFormat 16 | { 17 | get 18 | { 19 | if (_canFormat == null) 20 | _canFormat = f => f is T; 21 | 22 | return _canFormat; 23 | } 24 | set 25 | { 26 | _canFormat = value; 27 | } 28 | } 29 | 30 | public Func Formatter { get; set; } 31 | public Func Cast { get; set; } 32 | 33 | public string FormatValue(object obj) 34 | { 35 | if (Cast != null) 36 | return Formatter(Cast(obj)); 37 | 38 | return Formatter((T)obj); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /wiremock/mappings/2019-01-17_2019-02-17.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "065a8117-540c-388f-bb1f-21e815c3e073", 3 | "request": { 4 | "url": "/api/proxy/2019-01-17_2019-02-17", 5 | "method": "GET" 6 | }, 7 | "response": { 8 | "status": 200, 9 | "bodyFileName": "2019-01-17_2019-02-17.json", 10 | "headers": { 11 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 12 | "Content-Type": "application/json;charset=utf-8", 13 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 14 | "Vary": "Accept-Encoding, User-Agent", 15 | "X-Content-Type-Options": "nosniff", 16 | "X-Download-Options": "noopen", 17 | "x-frame-options": "DENY", 18 | "x-http2-stream-id": "49", 19 | "X-Permitted-Cross-Domain-Policies": "none", 20 | "x-xss-protection": "1; mode=block", 21 | "transfer-encoding": "chunked", 22 | "Connection": "keep-alive" 23 | } 24 | }, 25 | "uuid": "065a8117-540c-388f-bb1f-21e815c3e073" 26 | } -------------------------------------------------------------------------------- /wiremock/mappings/2019-02-17_2019-03-17.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "065a8117-540c-388f-bb1f-21e815c3e073", 3 | "request": { 4 | "url": "/api/proxy/2019-02-17_2019-03-17", 5 | "method": "GET" 6 | }, 7 | "response": { 8 | "status": 200, 9 | "bodyFileName": "2019-02-17_2019-03-17.json", 10 | "headers": { 11 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 12 | "Content-Type": "application/json;charset=utf-8", 13 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 14 | "Vary": "Accept-Encoding, User-Agent", 15 | "X-Content-Type-Options": "nosniff", 16 | "X-Download-Options": "noopen", 17 | "x-frame-options": "DENY", 18 | "x-http2-stream-id": "49", 19 | "X-Permitted-Cross-Domain-Policies": "none", 20 | "x-xss-protection": "1; mode=block", 21 | "transfer-encoding": "chunked", 22 | "Connection": "keep-alive" 23 | } 24 | }, 25 | "uuid": "065a8117-540c-388f-bb1f-21e815c3e073" 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Glauber D. Gasparotto Junior 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/NubankCli/bin/Debug/netcoreapp3.1/NubankCli.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "externalTerminal", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /wiremock/mappings/app-discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "04cc3736-6123-3e3a-8269-9df24355697e", 3 | "request": { 4 | "url": "/api/app/discovery", 5 | "method": "GET", 6 | "headers": { 7 | "Accept": { 8 | "equalTo": "application/json, text/json, text/x-json, text/javascript, application/xml, text/xml" 9 | } 10 | } 11 | }, 12 | "response": { 13 | "status": 200, 14 | "bodyFileName": "app-discovery.json", 15 | "headers": { 16 | "Content-Security-Policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 17 | "Content-Type": "application/json;charset=utf-8", 18 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 19 | "Vary": "Accept-Encoding, User-Agent", 20 | "X-Content-Type-Options": "nosniff", 21 | "X-Download-Options": "noopen", 22 | "X-Frame-Options": "DENY", 23 | "X-Permitted-Cross-Domain-Policies": "none", 24 | "X-XSS-Protection": "1; mode=block", 25 | "Connection": "keep-alive" 26 | } 27 | }, 28 | "uuid": "04cc3736-6123-3e3a-8269-9df24355697e" 29 | } -------------------------------------------------------------------------------- /wiremock/mappings/discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2e713a27-721c-3983-b53b-d82783dd09b3", 3 | "request": { 4 | "url": "/api/discovery", 5 | "method": "GET", 6 | "headers": { 7 | "Accept": { 8 | "equalTo": "application/json, text/json, text/x-json, text/javascript, application/xml, text/xml" 9 | } 10 | } 11 | }, 12 | "response": { 13 | "status": 200, 14 | "bodyFileName": "discovery.json", 15 | "headers": { 16 | "Content-Security-Policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 17 | "Content-Type": "application/json;charset=utf-8", 18 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 19 | "Vary": "Accept-Encoding, User-Agent", 20 | "X-Content-Type-Options": "nosniff", 21 | "X-Download-Options": "noopen", 22 | "X-Frame-Options": "DENY", 23 | "X-Permitted-Cross-Domain-Policies": "none", 24 | "X-XSS-Protection": "1; mode=block", 25 | "transfer-encoding": "chunked", 26 | "Connection": "keep-alive" 27 | } 28 | }, 29 | "uuid": "2e713a27-721c-3983-b53b-d82783dd09b3" 30 | } -------------------------------------------------------------------------------- /wiremock/mappings/Login.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "df90802c-fc85-365a-88eb-39598af9acda", 3 | "request": { 4 | "url": "/api/proxy/login", 5 | "method": "POST", 6 | "headers": { 7 | "Content-Type": { 8 | "equalTo": "application/json" 9 | }, 10 | "Accept": { 11 | "equalTo": "application/json, text/json, text/x-json, text/javascript, application/xml, text/xml" 12 | } 13 | } 14 | }, 15 | "response": { 16 | "status": 200, 17 | "bodyFileName": "Login.json", 18 | "headers": { 19 | "Cache-Control": "no-store", 20 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 21 | "Content-Type": "application/json;charset=utf-8", 22 | "Pragma": "no-cache", 23 | "X-Content-Type-Options": "nosniff", 24 | "X-Download-Options": "noopen", 25 | "x-frame-options": "DENY", 26 | "x-http2-stream-id": "3455", 27 | "X-Permitted-Cross-Domain-Policies": "none", 28 | "x-xss-protection": "1; mode=block", 29 | "Connection": "keep-alive" 30 | } 31 | }, 32 | "uuid": "df90802c-fc85-365a-88eb-39598af9acda" 33 | } -------------------------------------------------------------------------------- /wiremock/mappings/LoginAccessTokenTwoFactory.json.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4f8ac28f-7838-39e6-8d10-1b006c700edf", 3 | "request": { 4 | "url": "/api/proxy/lift", 5 | "method": "POST" 6 | }, 7 | "response": { 8 | "status": 200, 9 | "bodyFileName": "LoginAccessTokenTwoFactory.json.json", 10 | "headers": { 11 | "Cache-Control": "no-store", 12 | "content-security-policy": "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;", 13 | "Content-Type": "application/json;charset=utf-8", 14 | "Pragma": "no-cache", 15 | "Strict-Transport-Security": "max-age=31536000; includeSubdomains", 16 | "X-Content-Type-Options": "nosniff", 17 | "X-Download-Options": "noopen", 18 | "x-frame-options": "DENY", 19 | "x-http2-stream-id": "16123", 20 | "X-Permitted-Cross-Domain-Policies": "none", 21 | "x-signature": "ANNsWToAAALrLB9t2Gw2KbAA0rk8C-QDrv6Mw3OFfASo5wE2Lu1Mtas9EGSglI3GBMhNtkLNi0X9_xTz93HJ2TTYTb0Vt0oD5Yk2qt8P3e8M-gYYDjpBoDHvbDOxgbUcgH-0MBNsyUspd0Ojqc-U6EEke43upAU_LcVmB8RG2c5R5-Mwr9vZulOqLQU3", 22 | "x-xss-protection": "1; mode=block", 23 | "Connection": "keep-alive" 24 | } 25 | }, 26 | "uuid": "4f8ac28f-7838-39e6-8d10-1b006c700edf" 27 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Queries/savings.gql: -------------------------------------------------------------------------------- 1 | { 2 | viewer { 3 | savingsAccount { 4 | id feed, 5 | { 6 | id 7 | __typename 8 | title 9 | detail 10 | postDate 11 | ... on BarcodePaymentFailureEvent, 12 | { 13 | amount 14 | } 15 | ... on TransferInEvent { 16 | amount originAccount, 17 | { 18 | name 19 | } 20 | } 21 | ... on TransferOutEvent { 22 | amount destinationAccount, 23 | { 24 | name 25 | } 26 | } 27 | ... on TransferOutReversalEvent { 28 | amount 29 | } 30 | ... on BillPaymentEvent { 31 | amount 32 | } 33 | ... on DebitPurchaseEvent { 34 | amount 35 | } 36 | ... on BarcodePaymentEvent { 37 | amount 38 | } 39 | ... on DebitWithdrawalFeeEvent { 40 | amount 41 | } 42 | ... on DebitWithdrawalEvent { 43 | amount 44 | } 45 | ... on LendingTransferOutEvent { 46 | amount 47 | } 48 | ... on LendingTransferInEvent { 49 | amount 50 | } 51 | ... on PhoneRechargeSuccessEvent { 52 | amount 53 | } 54 | } 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Savings/GetSavingsAccountFeedResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace NubankSharp.Repositories.Api 5 | { 6 | public class GetSavingsAccountFeedResponse 7 | { 8 | public GetSavingsAccountFeedResponse() 9 | { 10 | Data = new DataResponse(); 11 | } 12 | 13 | [JsonProperty("data")] 14 | public DataResponse Data { get; set; } 15 | 16 | public class DataResponse 17 | { 18 | public DataResponse() 19 | { 20 | Viewer = new ViewerResponse(); 21 | } 22 | 23 | [JsonProperty("viewer")] 24 | public ViewerResponse Viewer { get; set; } 25 | } 26 | 27 | public class ViewerResponse 28 | { 29 | public ViewerResponse() 30 | { 31 | SavingsAccount = new SavingsAccount(); 32 | } 33 | 34 | [JsonProperty("savingsAccount")] 35 | public SavingsAccount SavingsAccount { get; set; } 36 | } 37 | 38 | public class SavingsAccount 39 | { 40 | public SavingsAccount() 41 | { 42 | Feed = new List(); 43 | } 44 | 45 | [JsonProperty("feed")] 46 | public List Feed { get; set; } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/NubankCli/NubankCli.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/NubankCli/NubankCli.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/NubankCli/NubankCli.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /src/NubankSharp/NubankSharp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5 4 | 1.0.0 5 | Glauber Gasparotto 6 | Importador de transações do NuBank e simples visualizador baseado em arquivos 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Balance/GetBalanceResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace NubankSharp.Repositories.Api 4 | { 5 | public class GetBalanceResponse 6 | { 7 | public GetBalanceResponse() 8 | { 9 | Data = new DataResponse(); 10 | } 11 | 12 | [JsonProperty("data")] 13 | public DataResponse Data { get; set; } 14 | 15 | public class DataResponse 16 | { 17 | public DataResponse() 18 | { 19 | Viewer = new ViewerResponse(); 20 | } 21 | 22 | [JsonProperty("viewer")] 23 | public ViewerResponse Viewer { get; set; } 24 | } 25 | 26 | public class ViewerResponse 27 | { 28 | public ViewerResponse() 29 | { 30 | SavingsAccount = new CurrentSavingsBalance(); 31 | } 32 | 33 | [JsonProperty("savingsAccount")] 34 | public CurrentSavingsBalance SavingsAccount { get; set; } 35 | } 36 | 37 | public class CurrentSavingsBalance 38 | { 39 | [JsonProperty("currentSavingsBalance")] 40 | public NetAmountBalance NetAmountBalance { get; set; } 41 | } 42 | 43 | public class NetAmountBalance 44 | { 45 | [JsonProperty("netAmount")] 46 | public decimal NetAmount { get; set; } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Events/EventCategory.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace NubankSharp.Repositories.Api 4 | { 5 | public enum EventCategory 6 | { 7 | Unknown, 8 | [EnumMember(Value = "transaction")] 9 | Transaction, 10 | [EnumMember(Value = "payment")] 11 | Payment, 12 | [EnumMember(Value = "bill_flow_paid")] 13 | BillFlowPaid, 14 | [EnumMember(Value = "welcome")] 15 | Welcome, 16 | [EnumMember(Value = "tutorial")] 17 | Tutorial, 18 | [EnumMember(Value = "customer_invitations_changed")] 19 | CustomerInvitationsChanged, 20 | [EnumMember(Value = "initial_account_limit")] 21 | InitialAccountLimit, 22 | [EnumMember(Value = "card_activated")] 23 | CardActivated, 24 | [EnumMember(Value = "transaction_reversed")] 25 | TransactionReversed, 26 | [EnumMember(Value = "account_limit_set")] 27 | AccountLimitSet, 28 | [EnumMember(Value = "customer_password_changed")] 29 | CustomerPasswordChanged, 30 | [EnumMember(Value = "bill_flow_closed")] 31 | BillFlowClosed, 32 | [EnumMember(Value = "customer_device_authorized")] 33 | CustomerDeviceAuthorized, 34 | [EnumMember(Value = "rewards_canceled")] 35 | RewardsCanceled, 36 | [EnumMember(Value = "rewards_signup")] 37 | RewardsSignup 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Files/JsonFileRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.Extensions; 3 | using System.IO; 4 | 5 | namespace NubankSharp.Repositories.Files 6 | { 7 | /// 8 | /// Essa classe pode trabalhar de 2 formas, 9 | /// 1) Pode ser uma representação 1x1 para um arquivo quando passado o path no construtor e não passado como args nos métodos 10 | /// 2) Pode ser uma representação 1xN, ou seja, como se fosse um repositório para um determinado tipo quando não passado o path no construtor 11 | /// 12 | /// 13 | public class JsonFileRepository 14 | { 15 | private readonly string _filePath; 16 | 17 | public JsonFileRepository(string filePath = null) 18 | { 19 | this._filePath = filePath; 20 | } 21 | 22 | public T GetFile(string filePath = null) 23 | { 24 | filePath ??= _filePath; 25 | if (File.Exists(filePath)) 26 | { 27 | string json = File.ReadAllText(filePath); 28 | return JsonConvert.DeserializeObject(json); 29 | } 30 | 31 | return default; 32 | } 33 | 34 | public void Save(T entity, string filePath = null) 35 | { 36 | filePath ??= _filePath; 37 | FileExtensions.CreateFolderIfNeeded(filePath); 38 | File.WriteAllText(filePath, JsonConvert.SerializeObject(entity, Formatting.Indented)); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Bills/BillSummary.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | namespace NubankSharp.Repositories.Api 5 | { 6 | public class BillSummary 7 | { 8 | [JsonProperty("due_date")] 9 | public DateTime DueDate { get; set; } 10 | 11 | [JsonProperty("close_date")] 12 | public DateTime CloseDate { get; set; } 13 | 14 | [JsonProperty("late_interest_rate")] 15 | public string LateInterestRate { get; set; } 16 | 17 | [JsonProperty("past_balance")] 18 | public long PastBalance { get; set; } 19 | 20 | [JsonProperty("late_fee")] 21 | public string LateFee { get; set; } 22 | 23 | [JsonProperty("effective_due_date")] 24 | public DateTime EffectiveDueDate { get; set; } 25 | 26 | [JsonProperty("total_balance")] 27 | public long TotalBalance { get; set; } 28 | 29 | [JsonProperty("interest_rate")] 30 | public string InterestRate { get; set; } 31 | 32 | [JsonProperty("interest")] 33 | public long Interest { get; set; } 34 | 35 | [JsonProperty("total_cumulative")] 36 | public long TotalCumulative { get; set; } 37 | 38 | [JsonProperty("paid")] 39 | public long Paid { get; set; } 40 | 41 | [JsonProperty("minimum_payment")] 42 | public long MinimumPayment { get; set; } 43 | 44 | [JsonProperty("open_date")] 45 | public DateTime OpenDate { get; set; } 46 | 47 | public long RemainingBalance { get; set; } 48 | public long RemainingMinimumPayment { get; set; } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/JwtDecoderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Text; 5 | 6 | namespace NubankSharp.Services 7 | { 8 | public static class JwtDecoderExtensions 9 | { 10 | public static JObject GetClaims(string jwt) 11 | { 12 | var base64UrlClaimsSet = GetBase64UrlClaimsSet(jwt); 13 | var claimsSet = DecodeBase64Url(base64UrlClaimsSet); 14 | 15 | try 16 | { 17 | return JObject.Parse(claimsSet); 18 | } 19 | catch (JsonReaderException e) 20 | { 21 | throw new FormatException(e.Message, e); 22 | } 23 | } 24 | 25 | private static string GetBase64UrlClaimsSet(string jwt) 26 | { 27 | var firstDotIndex = jwt.IndexOf('.'); 28 | var lastDotIndex = jwt.LastIndexOf('.'); 29 | 30 | if (firstDotIndex == -1 || lastDotIndex <= firstDotIndex) 31 | { 32 | throw new FormatException("The JWT should contain two periods."); 33 | } 34 | 35 | return jwt.Substring(firstDotIndex + 1, lastDotIndex - firstDotIndex - 1); 36 | } 37 | 38 | private static string DecodeBase64Url(string base64Url) 39 | { 40 | var base64 = base64Url 41 | .Replace('-', '+') 42 | .Replace('_', '/') 43 | .PadRight(base64Url.Length + (4 - base64Url.Length % 4) % 4, '='); 44 | 45 | return Encoding.UTF8.GetString(Convert.FromBase64String(base64)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/NubankSharp/Entities/Card.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp; 2 | using System.Diagnostics; 3 | using System.IO; 4 | 5 | namespace NubankSharp.Entities 6 | { 7 | [DebuggerDisplay("Card: {GetCardName()}")] 8 | public class Card 9 | { 10 | public const string NUCONTA_NAME = "nuconta"; 11 | public const string CREDIT_CARD_NAME = "credit-card"; 12 | public const string CREDIT_CARD_BY_MONTH_NAME = "credit-card-by-month"; 13 | 14 | public string UserName { get; } 15 | public string Name => GetCardName(); 16 | public int BankId => Constants.BANK_ID; 17 | public int Agency { get; set; } 18 | public int Account { get; set; } 19 | public CardType CardType { get; set; } 20 | public StatementType StatementType { get; set; } 21 | 22 | public Card(string userName, CardType cardType, StatementType statementType, int agency = 0, int account = 0) 23 | { 24 | UserName = userName; 25 | StatementType = statementType; 26 | Agency = agency; 27 | Account = account; 28 | CardType = cardType; 29 | } 30 | 31 | private string GetCardName() 32 | { 33 | string name; 34 | if (CardType == CardType.CreditCard) 35 | { 36 | name = CREDIT_CARD_NAME; 37 | 38 | if (StatementType == StatementType.ByMonth) 39 | name = CREDIT_CARD_BY_MONTH_NAME; 40 | } 41 | else 42 | { 43 | name = NUCONTA_NAME; 44 | } 45 | 46 | 47 | return name; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Login/LoginResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Authentication; 5 | 6 | namespace NubankSharp.Repositories.Api 7 | { 8 | public class LoginResponse 9 | { 10 | public string Token { get; private set; } 11 | public string RefreshToken { get; private set; } 12 | public Dictionary AutenticatedUrls { get; private set; } 13 | 14 | public LoginResponse(Dictionary response) 15 | { 16 | // Set Tokens 17 | if (response.Keys.Any(x => x == "error")) 18 | throw new AuthenticationException(response["error"].ToString()); 19 | 20 | if (!response.Keys.Any(x => x == "access_token")) 21 | throw new AuthenticationException("Unknow error occurred on trying to do login on Nubank using the entered credentials"); 22 | 23 | Token = response["access_token"].ToString(); 24 | 25 | if (response.ContainsKey("refresh_token")) 26 | RefreshToken = response["refresh_token"].ToString(); 27 | 28 | // Set urls 29 | var listLinks = (JObject)response["_links"]; 30 | var properties = listLinks.Properties(); 31 | var values = listLinks.Values(); 32 | this.AutenticatedUrls = listLinks 33 | .Properties() 34 | .Select(x => new KeyValuePair(x.Name, (string)listLinks[x.Name]["href"])) 35 | .ToDictionary(key => key.Key, key => key.Value); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/NuBankCli.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30320.27 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NubankCli", "NubankCli\NubankCli.csproj", "{5CDA2B7B-962B-4EA3-8138-C08F5D72471D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NubankSharp", "NubankSharp\NubankSharp.csproj", "{BAA7B5E8-687D-47E6-8B07-8F618A4733A1}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {5CDA2B7B-962B-4EA3-8138-C08F5D72471D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {5CDA2B7B-962B-4EA3-8138-C08F5D72471D}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {5CDA2B7B-962B-4EA3-8138-C08F5D72471D}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {5CDA2B7B-962B-4EA3-8138-C08F5D72471D}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {BAA7B5E8-687D-47E6-8B07-8F618A4733A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {BAA7B5E8-687D-47E6-8B07-8F618A4733A1}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {BAA7B5E8-687D-47E6-8B07-8F618A4733A1}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {BAA7B5E8-687D-47E6-8B07-8F618A4733A1}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {3C6162EE-9A6F-4DE1-B9F4-710F76FF2591} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /wiremock/__files/discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "register_prospect_savings_web": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect_savings_web", 3 | "register_prospect_savings_mgm": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect_savings_mgm", 4 | "pusher_auth_channel": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/pusher_auth_channel", 5 | "register_prospect_debit": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect_debit", 6 | "reset_password": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/reset_password", 7 | "register_prospect": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect", 8 | "register_prospect_savings_request_money": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect_savings_request_money", 9 | "register_prospect_global_web": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect_global_web", 10 | "register_prospect_c": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect_c", 11 | "request_password_reset": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/request_password_reset", 12 | "auth_gen_certificates": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/auth_gen_certificates", 13 | "login": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/login", 14 | "email_verify": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/email_verify", 15 | "auth_device_resend_code": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/auth_device_resend_code", 16 | "msat": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/msat" 17 | } -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatDateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Humanizer; 3 | 4 | namespace NubankSharp.Cli.Extensions.Formatters 5 | { 6 | public static class FormatDateTimeExtensions 7 | { 8 | public const string FORMAT_DATE_UNIVERSAL = "yyyy-MM-dd HH:mm:ss"; 9 | 10 | public static string HumanizeDefault(this DateTime date) 11 | { 12 | return date.Humanize(false); 13 | } 14 | 15 | public static string HumanizeDefault(this DateTime? date) 16 | { 17 | if (date == null) 18 | return "-"; 19 | 20 | return date.Value.Humanize(false); 21 | } 22 | 23 | public static string ToShortDate(this DateTime? date) 24 | { 25 | if (date == null) 26 | return "-"; 27 | 28 | return ToShortDate(date.Value); 29 | } 30 | 31 | public static string ToShortDate(this DateTime date) 32 | { 33 | return date.ToString("dd/MM/yyyy"); 34 | } 35 | 36 | public static string ToLongDate(this DateTime? date) 37 | { 38 | return ToLongDate(date.Value); 39 | } 40 | 41 | public static string ToLongDate(this DateTime date) 42 | { 43 | return date.ToString("dd/MM/yyyy HH:mm:ss"); 44 | } 45 | 46 | public static string ToLongDateNoSeconds(this DateTime date) 47 | { 48 | return date.ToString("dd/MM/yyyy HH:mm"); 49 | } 50 | 51 | public static string ToUniversalString(this DateTime date) 52 | { 53 | return date.ToString(FORMAT_DATE_UNIVERSAL); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/NubankCli/Program.cs: -------------------------------------------------------------------------------- 1 | using SysCommand.ConsoleApp; 2 | using System; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using NubankSharp.Extensions; 5 | using NubankSharp.Cli.Extensions; 6 | using NubankCli.Extensions.Configurations; 7 | using NubankSharp.Extensions.Tables; 8 | using NubankSharp.Entities; 9 | using NubankSharp.Repositories.Files; 10 | 11 | namespace NubankSharp 12 | { 13 | public partial class Program : Command 14 | { 15 | public static int Main() 16 | { 17 | //CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("nl-NL"); 18 | 19 | // Para os arquivos de configuração e dados do usuário: 20 | // 1) Usa a pasta do código como pasta root da execução quando estiver EM MODO DE DEBUG ou usando dotnet run 21 | // 2) Usa a pasta atual do código quando estiver em modo RELEASE, ou seja, quando é gerado o EXE (não gere o EXE em modo DEBUG para evitar confusões) 22 | var CONFIG_FOLDER = EnvironmentExtensions.ProjectRootOrExecutionDirectory; 23 | 24 | return App.RunApplication(() => 25 | { 26 | var app = new App(); 27 | app.Console.Verbose = Verbose.All; 28 | app.Console.ColorSuccess = ConsoleColor.DarkGray; 29 | 30 | var services = new ServiceCollection() 31 | .AddConfigurations(CONFIG_FOLDER, "settings.json") 32 | .ConfigureTables(); 33 | 34 | services.AddSingleton(typeof(JsonFileRepository<>), typeof(JsonFileRepository<>)); 35 | 36 | app.Items.SetServiceProvider(services.BuildServiceProvider()); 37 | 38 | return app; 39 | }); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /wiremock/__files/events.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "description": "Transaction2", 5 | "category": "transaction", 6 | "amount": 10000, 7 | "time": "2019-01-01T00:02:47Z", 8 | "title": "outros", 9 | "details": { 10 | "lat": -22.6951272, 11 | "lon": -46.9846185, 12 | "subcategory": "card_not_present" 13 | }, 14 | "id": "36559092-d2ea-4522-b298-c0503576ed1a", 15 | "_links": { 16 | "self": { 17 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/transaction2" 18 | } 19 | }, 20 | "href": "nuapp://transaction/transaction2" 21 | }, 22 | { 23 | "description": "Transaction1", 24 | "category": "transaction", 25 | "amount": 10000, 26 | "time": "2019-01-02T11:59:43Z", 27 | "title": "supermercado", 28 | "details": { 29 | "lat": -22.8115606, 30 | "lon": -47.0350431, 31 | "subcategory": "card_present" 32 | }, 33 | "id": "865dc486-52f1-44ca-8efe-0f8c4f4f92b7", 34 | "_links": { 35 | "self": { 36 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/transaction1" 37 | } 38 | }, 39 | "href": "nuapp://transaction/transaction1" 40 | } 41 | ], 42 | "customer_id": "1901214c-be0a-4ec1-816c-9f3b18a2be7e", 43 | "as_of": "2019-01-01T20:43:43.708Z", 44 | "_links": { 45 | "updates": { 46 | "href": "" 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/NubankSharp/Entities/NuUser.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using NubankSharp.Services; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | namespace NubankSharp.Entities 9 | { 10 | [Serializable] 11 | public class NuUser 12 | { 13 | public string UserName { get; } 14 | public string Password { get; private set; } 15 | 16 | public string Token { get; set; } 17 | public string RefreshToken { get; set; } 18 | public string CertificateBase64 { get; set; } 19 | public string CertificateCryptoBase64 { get; set; } 20 | 21 | public Dictionary AutenticatedUrls { get; set; } 22 | 23 | public NuUser(string userName, string password) 24 | { 25 | this.UserName = userName; 26 | this.Password = password; 27 | } 28 | 29 | public DateTime GetExpiredDate() 30 | { 31 | if (Token == null) 32 | return DateTime.MinValue; 33 | 34 | var jobject = JwtDecoderExtensions.GetClaims(Token); 35 | var exp = jobject["exp"].Value(); 36 | DateTimeOffset dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(exp); 37 | return dateTimeOffset.LocalDateTime; 38 | } 39 | 40 | public bool IsValid() 41 | { 42 | var exp = GetExpiredDate(); 43 | if (exp > DateTime.Now) 44 | return true; 45 | 46 | return false; 47 | } 48 | 49 | public void CleanPassword() 50 | { 51 | Password = null; 52 | } 53 | 54 | 55 | public string GetLoginType() 56 | { 57 | return string.IsNullOrWhiteSpace(CertificateBase64) ? "QRCODE" : "CERTIFICATE"; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /wiremock/__files/2019-01-17_2019-02-17.json: -------------------------------------------------------------------------------- 1 | { 2 | "bill": { 3 | "id": "5c8dca9d-0106-4bcb-a0a4-1e1353b8348b", 4 | "state": "overdue", 5 | "summary": { 6 | "remaining_balance": 0, 7 | "due_date": "2019-02-24", 8 | "close_date": "2019-02-17", 9 | "past_balance": 0, 10 | "effective_due_date": "2019-02-24", 11 | "total_balance": 200, 12 | "interest_rate": "0.14", 13 | "interest": 0, 14 | "total_cumulative": 200, 15 | "paid": 0, 16 | "minimum_payment": 200, 17 | "remaining_minimum_payment": 0, 18 | "open_date": "2019-01-17" 19 | }, 20 | "_links": { 21 | "boleto_email": { 22 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/boleto_email" 23 | }, 24 | "barcode": { 25 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/barcode" 26 | }, 27 | "invoice_email": { 28 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/invoice_email" 29 | }, 30 | "self": { 31 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/self" 32 | } 33 | }, 34 | "line_items": [ 35 | { 36 | "amount": 100, 37 | "index": 0, 38 | "title": "Transaction1", 39 | "post_date": "2019-02-01", 40 | "id": "cb8515d6-85dd-4cb9-95bf-4defd3f84e69", 41 | "href": "nuapp://transaction/transaction1", 42 | "category": "transporte", 43 | "charges": 5 44 | }, 45 | { 46 | "amount": 100, 47 | "index": 0, 48 | "title": "Transaction2", 49 | "post_date": "2019-02-02", 50 | "id": "6c1aff9f-03be-4356-a40e-97ae39a18f9b", 51 | "href": "nuapp://transaction/transaction2", 52 | "category": "outros", 53 | "charges": 12 54 | } 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /wiremock/__files/2019-02-17_2019-03-17.json: -------------------------------------------------------------------------------- 1 | { 2 | "bill": { 3 | "id": "5c8dca9d-0106-4bcb-a0a4-1e1353b8348b", 4 | "state": "overdue", 5 | "summary": { 6 | "due_date": "2019-03-24", 7 | "close_date": "2019-03-17", 8 | "late_interest_rate": "0.15", 9 | "past_balance": 0, 10 | "late_fee": "0.02", 11 | "effective_due_date": "2019-03-24", 12 | "total_balance": 100, 13 | "interest_rate": "0.14", 14 | "interest": 0, 15 | "total_cumulative": 100, 16 | "paid": 0, 17 | "minimum_payment": 0, 18 | "open_date": "2019-02-17" 19 | }, 20 | "_links": { 21 | "boleto_email": { 22 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/boleto_email" 23 | }, 24 | "barcode": { 25 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/barcode" 26 | }, 27 | "invoice_email": { 28 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/invoice_email" 29 | }, 30 | "self": { 31 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/self" 32 | } 33 | }, 34 | "line_items": [ 35 | { 36 | "amount": 50, 37 | "index": 1, 38 | "title": "Pag*Autocenterescapjr", 39 | "post_date": "2019-02-17", 40 | "id": "cb8515d6-85dd-4cb9-95bf-4defd3f84e69", 41 | "href": "nuapp://transaction/transaction1", 42 | "category": "transporte", 43 | "charges": 5 44 | }, 45 | { 46 | "amount": 50, 47 | "index": 1, 48 | "title": "Mercpago*Mercadolivre", 49 | "post_date": "2019-02-17", 50 | "id": "6c1aff9f-03be-4356-a40e-97ae39a18f9b", 51 | "href": "nuapp://transaction/transaction2", 52 | "category": "outros", 53 | "charges": 12 54 | } 55 | ] 56 | } 57 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/GqlQueryRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.Extensions; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace NubankSharp.Repositories.Api 7 | { 8 | /// 9 | /// Essa classe pode trabalhar de 2 formas, 10 | /// 1) Pode ser uma representação 1x1 para um arquivo quando passado o path no construtor e não passado como args nos métodos 11 | /// 2) Pode ser uma representação 1xN, ou seja, como se fosse um repositório para um determinado tipo quando não passado o path no construtor 12 | /// 13 | /// 14 | public class GqlQueryRepository : IGqlQueryRepository 15 | { 16 | private readonly string _folderPath; 17 | private Dictionary _queries = new(); 18 | 19 | public GqlQueryRepository(string folderPath = null) 20 | { 21 | this._folderPath = folderPath; 22 | var assembly = typeof(GqlQueryRepository).Assembly; 23 | var resourceFolder = "NubankSharp.Repositories.Api.Queries."; 24 | 25 | foreach (var name in assembly.GetManifestResourceNames()) 26 | { 27 | if (name.StartsWith(resourceFolder)) 28 | { 29 | var nameWithouFolder = Path.GetFileNameWithoutExtension(name.Replace(resourceFolder, "")); 30 | using (var reader = new StreamReader(assembly.GetManifestResourceStream(name))) 31 | { 32 | _queries.Add(nameWithouFolder, reader.ReadToEnd()); 33 | } 34 | } 35 | } 36 | } 37 | 38 | public string GetGql(string queryName) 39 | { 40 | // Da prioridade para o arquivo caso exista, do contrário, tenta encontrar nos resources 41 | if (!string.IsNullOrWhiteSpace(this._folderPath)) 42 | { 43 | var filePath = Path.Combine(this._folderPath, queryName + ".gql"); 44 | if (File.Exists(filePath)) 45 | return File.ReadAllText(filePath); 46 | } 47 | 48 | return _queries[queryName]; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/NubankCli/Commands/Statements/StatementTableConfig.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp.Extensions.Tables; 2 | using NubankSharp.Entities; 3 | using NubankSharp.Extensions; 4 | using NubankSharp.Cli.Extensions.Formatters; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace NubankSharp.Commands.Statements 9 | { 10 | public class StatementTableConfig : ITableConfig 11 | { 12 | public IEnumerable> GetTableColumns() 13 | { 14 | yield return new TableColumn 15 | { 16 | Name = "Cartão".ToUpper(), 17 | ValueFormatter = (s) => (s.Card?.Name) ?? "-" 18 | }; 19 | 20 | yield return new TableColumn 21 | { 22 | Name = "Início".ToUpper(), 23 | ValueFormatter = (s) => s.Start.ToString("yyyy/MM/dd"), 24 | }; 25 | 26 | yield return new TableColumn 27 | { 28 | Name = "Fim".ToUpper(), 29 | ValueFormatter = (s) => s.End.ToString("yyyy/MM/dd"), 30 | }; 31 | 32 | yield return new TableColumn 33 | { 34 | Name = "Entrada".ToUpper(), 35 | ValueFormatter = (s) => 36 | { 37 | var tIn = s.Transactions.GetIn(); 38 | return $"{tIn.Total().Format()} ({tIn.Count()})"; 39 | } 40 | }; 41 | 42 | yield return new TableColumn 43 | { 44 | Name = "Saída".ToUpper(), 45 | ValueFormatter = (s) => 46 | { 47 | var tOut = s.Transactions.GetOut(); 48 | return $"{tOut.Total().Format()} ({tOut.Count()})"; 49 | } 50 | }; 51 | 52 | yield return new TableColumn 53 | { 54 | Name = "Total".ToUpper(), 55 | ValueFormatter = (s) => $"{s.Transactions.Total().Format()} ({s.Transactions.Count()})", 56 | }; 57 | 58 | yield return new TableColumn 59 | { 60 | Name = "Tipo de extrato".ToUpper(), 61 | ValueFormatter = (s) => s.StatementType.ToString(), 62 | OnlyInWide = true 63 | }; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /wiremock/__files/bills.json: -------------------------------------------------------------------------------- 1 | { 2 | "bills": [ 3 | { 4 | "state": "open", 5 | "summary": { 6 | "due_date": "2019-03-24", 7 | "close_date": "2019-03-17", 8 | "late_interest_rate": "0.15", 9 | "past_balance": 0, 10 | "late_fee": "0.02", 11 | "effective_due_date": "2019-03-24", 12 | "total_balance": 100, 13 | "interest_rate": "0.14", 14 | "interest": 0, 15 | "total_cumulative": 100, 16 | "paid": 0, 17 | "minimum_payment": 0, 18 | "open_date": "2019-02-17" 19 | }, 20 | "_links": { 21 | "self": { 22 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/2019-02-17_2019-03-17" 23 | }, 24 | "barcode": { 25 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/barcode" 26 | }, 27 | "boleto_email": { 28 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/boleto_email" 29 | } 30 | } 31 | }, 32 | { 33 | "id": "f6d9bf8d-b1df-4aaa-bbe5-97fa0eb41e24", 34 | "state": "overdue", 35 | "summary": { 36 | "remaining_balance": 0, 37 | "due_date": "2019-02-24", 38 | "close_date": "2019-02-17", 39 | "past_balance": 0, 40 | "effective_due_date": "2019-02-24", 41 | "total_balance": 200, 42 | "interest_rate": "0.14", 43 | "interest": 0, 44 | "total_cumulative": 200, 45 | "paid": 0, 46 | "minimum_payment": 200, 47 | "remaining_minimum_payment": 0, 48 | "open_date": "2019-01-17" 49 | }, 50 | "_links": { 51 | "self": { 52 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/2019-01-17_2019-02-17" 53 | } 54 | } 55 | } 56 | ], 57 | "_links": { 58 | "open": { 59 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/open" 60 | }, 61 | "future": { 62 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/future" 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Transactions/TransactionTableConfig.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp.Extensions.Tables; 2 | using NubankSharp.Entities; 3 | using NubankSharp.Cli.Extensions.Formatters; 4 | using System.Collections.Generic; 5 | 6 | namespace NubankSharp.Commands.Transactions 7 | { 8 | public class TransactionTableConfig : ITableConfig 9 | { 10 | public IEnumerable> GetTableColumns() 11 | { 12 | yield return new TableColumn 13 | { 14 | Name = "Id".ToUpper(), 15 | ValueFormatter = (t) => t.Id.Format(), 16 | ValueFormatterWide = (t) => t.Id.ToString(), 17 | }; 18 | 19 | yield return new TableColumn 20 | { 21 | Name = "Data postagem".ToUpper(), 22 | ValueFormatter = (t) => t.PostDate.ToLongDateNoSeconds() 23 | }; 24 | 25 | yield return new TableColumn 26 | { 27 | Name = "Data event".ToUpper(), 28 | ValueFormatter = (t) => t.EventDate.ToLongDateNoSeconds() 29 | }; 30 | 31 | yield return new TableColumn 32 | { 33 | Name = "Nome".ToUpper(), 34 | ValueFormatter = (t) => t.GetNameFormatted() 35 | }; 36 | 37 | yield return new TableColumn 38 | { 39 | Name = "Valor".ToUpper(), 40 | ValueFormatter = (t) => t.Value.Format() 41 | }; 42 | 43 | yield return new TableColumn 44 | { 45 | Name = "Cartão".ToUpper(), 46 | ValueFormatter = (t) => t.CardName, 47 | }; 48 | 49 | yield return new TableColumn 50 | { 51 | Name = "Categoria".ToUpper(), 52 | ValueFormatter = (t) => t.Category, 53 | }; 54 | 55 | 56 | yield return new TableColumn 57 | { 58 | Name = "Lat".ToUpper(), 59 | ValueFormatter = (t) => t.Latitude.ToString(), 60 | OnlyInWide = true 61 | }; 62 | 63 | yield return new TableColumn 64 | { 65 | Name = "Lon".ToUpper(), 66 | ValueFormatter = (t) => t.Longitude.ToString(), 67 | OnlyInWide = true 68 | }; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace NubankSharp.Cli.Extensions.Formatters 8 | { 9 | public static class FormatObjectExtensions 10 | { 11 | public static List Formatters = new List 12 | { 13 | new FormatConditional 14 | { 15 | Formatter = obj => 16 | { 17 | var formatter = Formatters.FirstOrDefault(f => f.CanFormat(obj.Value) == true); 18 | if (formatter == null) 19 | return obj?.Value?.ToString(); 20 | 21 | return formatter.FormatValue(obj.Value); 22 | } 23 | }, 24 | 25 | new FormatConditional 26 | { 27 | CanFormat = f => f is Guid || (f is string && Guid.TryParse(f.ToString(), out _)), 28 | Cast = f => new Guid(f.ToString()), 29 | Formatter = f => f.HumanizeDefault() 30 | }, 31 | 32 | new FormatConditional { Formatter = f => f?.Trim()?.HumanizeDefault() }, 33 | new FormatConditional { Formatter = f => f.HumanizeDefault() }, 34 | new FormatConditional { Formatter = f => f.HumanizeDefault() }, 35 | new FormatConditional { Formatter = f => f.HumanizeDefault() }, 36 | new FormatConditional { Formatter = f => f.HumanizeDefault() }, 37 | new FormatConditional { Formatter = f => f.HumanizeDefault() }, 38 | new FormatConditional 39 | { 40 | Formatter = l => 41 | { 42 | var list = l.Cast(); 43 | return String.Join(", ", list?.Select(f => f.Format()) ?? new string[] { }).Trim(); 44 | } 45 | } 46 | }; 47 | 48 | public static string Format(this object obj, params Type[] ignore) 49 | { 50 | string value = null; 51 | 52 | if (ignore?.Contains(obj?.GetType()) == false) 53 | { 54 | var formatter = Formatters.FirstOrDefault(f => f.CanFormat(obj) == true); 55 | 56 | if (formatter != null) 57 | value = formatter.FormatValue(obj); 58 | } 59 | 60 | if (value == null) 61 | value = obj?.ToString(); 62 | 63 | return value; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Imports/ImportDebitCommand.cs: -------------------------------------------------------------------------------- 1 | using SysCommand.ConsoleApp; 2 | using System; 3 | using NubankSharp.Services; 4 | using NubankSharp.Extensions; 5 | using System.IO; 6 | using NubankSharp.Entities; 7 | using System.Collections.Generic; 8 | using NubankSharp.Cli.Extensions.Formatters; 9 | using NubankSharp.Repositories.Files; 10 | 11 | namespace NubankSharp.Cli 12 | { 13 | public partial class ImportCommand : Command 14 | { 15 | public void ImportDebit(DateTime? start = null, DateTime? end = null, int agency = 0, int account = 0) 16 | { 17 | try 18 | { 19 | // 1) Obtem as transações da NuConta 20 | var user = this.GetCurrentUser(); 21 | var card = new Card(user.UserName, CardType.NuConta, StatementType.ByMonth, agency, account); 22 | var nuApi = this.CreateNuApiByUser(user, nameof(ImportDebit)); 23 | var transations = nuApi.GetDebitTransactions(start, end); 24 | 25 | // 2) Converte em extratos mensais para salvar um arquivo por mês 26 | var statementFileRepository = new StatementFileRepository(); 27 | var statements = transations.ToStatementByMonth(card); 28 | foreach (var s in statements) 29 | statementFileRepository.Save(s, this.GetStatementFileName(s)); 30 | 31 | // 3) OUTPUT das transações 32 | var allSummary = transations.Summary(); 33 | 34 | App.Console.Success($" "); 35 | App.Console.Success($"TOTAL (ENTRADA): " + $"{allSummary.ValueIn.Format()} ({allSummary.CountIn})"); 36 | App.Console.Success($"TOTAL (SAÍDA) : " + $"{allSummary.ValueOut.Format()} ({allSummary.CountOut})"); 37 | App.Console.Success($"TOTAL : " + $"{allSummary.ValueTotal.Format()} ({allSummary.CountTotal})"); 38 | 39 | App.Console.Write($" "); 40 | this.ViewFormatted(statements); 41 | 42 | if (statements.Count > 0) 43 | { 44 | App.Console.Warning($" "); 45 | App.Console.Warning($"TRANSAÇÕES IMPORTADAS EM:"); 46 | App.Console.Warning($" {Path.GetFullPath(this.GetCardPath(card))}"); 47 | } 48 | 49 | App.Console.Warning($" "); 50 | App.Console.Warning("Diferenças no saldo importado pode ocorrer devido aos juros adicionais que não estão no extrato da sua NuConta"); 51 | } 52 | catch (Exception ex) 53 | { 54 | this.ShowApiException(ex); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/NubankCli/Commands/Transactions/GetCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Commands.Transactions 2 | { 3 | using SysCommand.ConsoleApp; 4 | using SysCommand.Mapping; 5 | using System; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using NubankSharp.Extensions; 8 | using System.Linq; 9 | using System.Linq.Dynamic.Core; 10 | using NubankSharp.Cli.Extensions.Formatters; 11 | using NubankSharp.Repositories.Files; 12 | 13 | public partial class TransactionCommand : Command 14 | { 15 | 16 | [Action(Name = "get")] 17 | public void Get( 18 | EntityNames type, 19 | [Argument(ShortName = 'i', LongName = "id-or-name")] string id = null, 20 | [Argument(ShortName = 'w', LongName = "where")] string where = null, 21 | [Argument(ShortName = 's', LongName = "sort")] string sort = "EventDate DESC", 22 | [Argument(ShortName = 'P', LongName = "page")] int page = 1, 23 | [Argument(ShortName = 'S', LongName = "page-size")] int pageSize = 100, 24 | [Argument(ShortName = 'A', LongName = "auto-page")] bool autoPage = true, 25 | [Argument(ShortName = 'o', LongName = "output")] string outputFormat = null 26 | ) 27 | { 28 | try 29 | { 30 | var statementFileRepository = new StatementFileRepository(); 31 | var transactions = statementFileRepository.GetTransactions(this.GetUserPath(this.GetCurrentUser()), id, where, null, sort); 32 | var summary = transactions.Summary(); 33 | 34 | App.Console.Success($" "); 35 | App.Console.Success($"TOTAL (ENTRADA): " + $"{summary.ValueIn.Format()} ({summary.CountIn})"); 36 | App.Console.Success($"TOTAL (SAÍDA) : " + $"{summary.ValueOut.Format()} ({summary.CountOut})"); 37 | App.Console.Success($"TOTAL : " + $"{summary.ValueTotal.Format()} ({summary.CountTotal})"); 38 | App.Console.Success($" "); 39 | 40 | if (page <= 0 || pageSize <= 0) 41 | { 42 | this.ViewFormatted(transactions, outputFormat); 43 | } 44 | else 45 | { 46 | this.ViewPagination(page, currentPage => 47 | { 48 | var pageResult = transactions.AsQueryable().PageResult(currentPage, pageSize); 49 | return pageResult; 50 | }, autoPage, outputFormat); 51 | } 52 | 53 | } 54 | catch (Exception ex) 55 | { 56 | this.ShowApiException(ex); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/EndPointApi.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace NubankSharp.Repositories.Api 6 | { 7 | public class EndPointApi 8 | { 9 | private readonly string DiscoveryUrl; 10 | private readonly string DiscoveryAppUrl; 11 | private readonly NuHttpClient _client; 12 | 13 | public Dictionary TopLevelUrs { get; private set; } 14 | public Dictionary AppUrls { get; private set; } 15 | public Dictionary AutenticatedUrls => this._client.User.AutenticatedUrls; 16 | 17 | public string Login => GetTopLevelUrl("login"); 18 | public string ResetPassword => GetTopLevelUrl("reset_password"); 19 | public string Lift => GetAppUrl("lift"); 20 | public string Token => GetAppUrl("token"); 21 | public string GenCertificate => GetAppUrl("gen_certificate"); 22 | public string Events => GetAutenticatedUrl("events"); 23 | public string BillsSummary => GetAutenticatedUrl("bills_summary"); 24 | public string GraphQl => GetAutenticatedUrl("ghostflame"); 25 | 26 | public EndPointApi(NuHttpClient httpClient) 27 | { 28 | _client = httpClient; 29 | DiscoveryUrl = $"{httpClient.NubankUrl}/api/discovery"; 30 | DiscoveryAppUrl = $"{httpClient.NubankUrl}/api/app/discovery"; 31 | } 32 | 33 | public string GetTopLevelUrl(string key) 34 | { 35 | if (TopLevelUrs == null) 36 | TopLevelUrs = Discover(); 37 | return GetEndPoint(key, TopLevelUrs); 38 | } 39 | 40 | public string GetAppUrl(string key) 41 | { 42 | if (AppUrls == null) 43 | AppUrls = DiscoverApp(); 44 | return GetEndPoint(key, AppUrls); 45 | } 46 | 47 | public string GetAutenticatedUrl(string key) 48 | { 49 | return GetEndPoint(key, AutenticatedUrls); 50 | } 51 | 52 | private Dictionary Discover() 53 | { 54 | return _client.Get>(nameof(DiscoveryUrl), DiscoveryUrl, out _); 55 | } 56 | 57 | private Dictionary DiscoverApp() 58 | { 59 | var response = _client.Get>(nameof(DiscoveryAppUrl), DiscoveryAppUrl, out _); 60 | 61 | return response 62 | .Where(x => x.Value is string) 63 | .Select(x => new KeyValuePair(x.Key, x.Value.ToString())) 64 | .ToDictionary(x => x.Key, x => x.Value.ToString()); 65 | } 66 | 67 | public static string GetEndPoint(string key, Dictionary source) 68 | { 69 | if (!source.ContainsKey(key)) 70 | return null; 71 | 72 | return source[key]; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Bills/GetBillResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | 8 | namespace NubankSharp.Repositories.Api 9 | { 10 | public class GetBillResponse 11 | { 12 | public Bill Bill { get; set; } 13 | } 14 | 15 | [DebuggerDisplay("{State} OpenDate: {Summary.OpenDate} CloseDate: {Summary.CloseDate}")] 16 | public class Bill 17 | { 18 | public Guid? Id { get; set; } 19 | public BillState State { get; set; } 20 | public BillSummary Summary { get; set; } 21 | 22 | [JsonProperty("line_items")] 23 | public List LineItems { get; set; } 24 | 25 | [JsonProperty("_links")] 26 | public SelfLink Links { get; set; } 27 | 28 | public IEnumerable GetTransactions() 29 | { 30 | return LineItems.Select(f => new Transaction(f)); 31 | } 32 | } 33 | 34 | [DebuggerDisplay("{PostDate} {Title} {Amount} {Index}/{Charges} ({Category})")] 35 | public class BillTransaction 36 | { 37 | public long Amount { get; set; } 38 | public int Index { get; set; } 39 | public string Title { get; set; } 40 | 41 | [JsonProperty("post_date")] 42 | public DateTime PostDate { get; set; } 43 | public DateTime EventDate 44 | { 45 | get 46 | { 47 | // Quando tem evento, a data vem no formato UTC e precisa converter pra pt-br 48 | if (Event != null) 49 | return TimeZoneInfo.ConvertTimeFromUtc(EventDateUtc, Constants.BR_TIME_ZONE); 50 | 51 | // Quando NÃO tem evento é usado entao o PostDate QUE NÃO ESTÁ EM UTC, por isso não pode converter, 52 | // do contrário a data do evento seria um dia antes da compra 53 | return EventDateUtc; 54 | } 55 | } 56 | 57 | public DateTime EventDateUtc 58 | { 59 | get 60 | { 61 | // O nubank só expoe na timeline de eventos a primeira compra parcelada, 62 | // as demais parcelas não aparecem na timeline. Devido a isso, apenas 63 | // a primeira compra vai ter a data real da timeline, as demais vão com o horario 00:00 64 | // OBS: Se existir compras futuras no nubank, talvez aparece na timeline antes do débito real, 65 | // isso pode ser um problema pois a data da compra será a da timeline e não a do débito. (VERIFICAR) 66 | if (Event?.Time != null && Index == 0) 67 | return Event.Time.UtcDateTime; 68 | 69 | return PostDate; 70 | } 71 | } 72 | 73 | public Guid Id { get; set; } 74 | public string Category { get; set; } 75 | public int Charges { get; set; } 76 | public string Href { get; set; } 77 | 78 | public Event Event { get; set; } 79 | public bool IsBillPaymentLastBill { get; set; } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /wiremock/__files/nuconta.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "viewer": { 4 | "savingsAccount": { 5 | "id": "1c122d31-1557-4d0d-a784-590d6b4a34e8", 6 | "feed": [ 7 | { 8 | "id": "7bf639c5-5a7b-4bde-9cbd-3a313b66ae64", 9 | "__typename": "BillPaymentEvent", 10 | "title": "Pagamento da fatura", 11 | "detail": "Cartão Nubank - R$ 3.203,70", 12 | "postDate": "2019-02-07", 13 | "amount": null 14 | }, 15 | { 16 | "id": "e0189407-d8e7-488e-af12-771dedc99e3d", 17 | "__typename": "BarcodePaymentFailureEvent", 18 | "title": "Pagamento devolvido", 19 | "detail": "TRIBUTOS", 20 | "postDate": "2019-02-06", 21 | "amount": 1488.09 22 | }, 23 | { 24 | "id": "8fd344a4-bc68-4bfd-a408-80f620999538", 25 | "__typename": "BarcodePaymentEvent", 26 | "title": "Pagamento efetuado", 27 | "detail": "TRIBUTOS", 28 | "postDate": "2019-02-05", 29 | "amount": 1488.09 30 | }, 31 | { 32 | "id": "a129b334-fa8b-43dd-bafd-6b06b22acafe", 33 | "__typename": "TransferOutEvent", 34 | "title": "Transferência enviada", 35 | "detail": "Junior - R$ 2.500,00", 36 | "postDate": "2019-02-04", 37 | "amount": 2500.0, 38 | "destinationAccount": { 39 | "name": "Junior" 40 | } 41 | }, 42 | { 43 | "id": "14a8b3ac-2b27-4c66-8f89-7dbda6ec5787", 44 | "__typename": "DebitPurchaseEvent", 45 | "title": "Compra no débito", 46 | "detail": "Snook Bar", 47 | "postDate": "2019-02-03", 48 | "amount": 18.0 49 | }, 50 | { 51 | "id": "df36e6db-a609-4a8b-89c3-2ed32adb895e", 52 | "__typename": "TransferInEvent", 53 | "title": "Transferência recebida", 54 | "detail": "R$ 33,00", 55 | "postDate": "2019-02-02", 56 | "amount": 33.0, 57 | "originAccount": { 58 | "name": "Pedro" 59 | } 60 | }, 61 | { 62 | "id": "f502898a-7977-47c3-9e85-d46bafe25fd8", 63 | "__typename": "WelcomeEvent", 64 | "title": "Bem vindo à sua conta!", 65 | "detail": "Marcio \nBanco 260 - Nu Pagamentos S.A.\nAgência 0001\nConta 0001111-1", 66 | "postDate": "2019-02-01" 67 | } 68 | ] 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Imports/ImportCreditCommand.cs: -------------------------------------------------------------------------------- 1 | using SysCommand.ConsoleApp; 2 | using System; 3 | using NubankSharp.Services; 4 | using NubankSharp.Extensions; 5 | using System.IO; 6 | using NubankSharp.Entities; 7 | using System.Collections.Generic; 8 | using NubankSharp.Cli.Extensions.Formatters; 9 | using System.Linq; 10 | using NubankSharp.Repositories.Files; 11 | using NubankSharp.Repositories.Api.Services; 12 | 13 | namespace NubankSharp.Cli 14 | { 15 | public partial class ImportCommand : Command 16 | { 17 | public void ImportCredit(DateTime? start = null, DateTime? end = null, StatementType statementType = StatementType.ByBill) 18 | { 19 | try 20 | { 21 | var user = this.GetCurrentUser(); 22 | var card = new Card(user.UserName, CardType.CreditCard, statementType); 23 | var nuApi = this.CreateNuApiByUser(user, nameof(ImportCredit)); 24 | 25 | // 1) Obtem os extratos separado de forma mensal ou como boletos 26 | List statements; 27 | if (statementType == StatementType.ByMonth) 28 | { 29 | var transactions = nuApi.GetCreditTransactions(start, end); 30 | statements = transactions.ToStatementByMonth(card); 31 | } 32 | else 33 | { 34 | var bills = nuApi.GetBills(start, end); 35 | statements = bills.ToStatementByBill(card); 36 | } 37 | 38 | // 2) Converte em extratos mensais para salvar um arquivo por mês 39 | var statementFileRepository = new StatementFileRepository(); 40 | foreach (var e in statements) 41 | statementFileRepository.Save(e, this.GetStatementFileName(e)); 42 | 43 | statements = statements.ExcludeBillPaymentLastBill(); 44 | 45 | // 3) OUTPUT das transações 46 | var allTransactions = statements.GetTransactions(); 47 | var allSummary = allTransactions.Summary(); 48 | 49 | App.Console.Success($" "); 50 | App.Console.Success($"TOTAL (ENTRADA): " + $"{allSummary.ValueIn.Format()} ({allSummary.CountIn})"); 51 | App.Console.Success($"TOTAL (SAÍDA) : " + $"{allSummary.ValueOut.Format()} ({allSummary.CountOut})"); 52 | App.Console.Success($"TOTAL : " + $"{allSummary.ValueTotal.Format()} ({allSummary.CountTotal})"); 53 | 54 | App.Console.Write($" "); 55 | this.ViewFormatted(statements); 56 | 57 | if (statements.Count > 0) 58 | { 59 | App.Console.Warning($" "); 60 | App.Console.Warning($"TRANSAÇÕES IMPORTADAS EM:"); 61 | App.Console.Warning($" {Path.GetFullPath(this.GetCardPath(card))}"); 62 | } 63 | 64 | App.Console.Warning($" "); 65 | App.Console.Warning("Transações do tipo 'Pagamento Recebido' foram importadas, mas não estão sendo consideradas nas somatórias para dar coerência com os totais de cada boleto"); 66 | } 67 | catch (Exception ex) 68 | { 69 | this.ShowApiException(ex); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Converters/TolerantEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | 6 | namespace NubankSharp.Repositories.Api 7 | { 8 | /// 9 | /// Code extracted from https://stackoverflow.com/questions/22752075/how-can-i-ignore-unknown-enum-values-during-json-deserialization 10 | /// 11 | class TolerantEnumConverter : JsonConverter 12 | { 13 | public override bool CanConvert(Type objectType) 14 | { 15 | var type = IsNullableType(objectType) ? Nullable.GetUnderlyingType(objectType) : objectType; 16 | return type.IsEnum; 17 | } 18 | 19 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 20 | { 21 | var isNullable = IsNullableType(objectType); 22 | var enumType = isNullable ? Nullable.GetUnderlyingType(objectType) : objectType; 23 | 24 | var names = Enum.GetNames(enumType); 25 | var options = names.Select(n => (Enum)Enum.Parse(enumType, n)); 26 | 27 | if (reader.TokenType == JsonToken.String) 28 | { 29 | var enumText = reader.Value.ToString(); 30 | 31 | if (!string.IsNullOrEmpty(enumText)) 32 | { 33 | var match = options 34 | .FirstOrDefault(n => { 35 | return string.Equals(n.GetJsonValue(), enumText, StringComparison.OrdinalIgnoreCase); 36 | }); 37 | 38 | if (match != null) 39 | { 40 | return match; 41 | } 42 | } 43 | } 44 | else if (reader.TokenType == JsonToken.Integer) 45 | { 46 | var enumVal = Convert.ToInt32(reader.Value); 47 | var values = (int[])Enum.GetValues(enumType); 48 | if (values.Contains(enumVal)) 49 | { 50 | return Enum.Parse(enumType, enumVal.ToString()); 51 | } 52 | } 53 | 54 | if (!isNullable) 55 | { 56 | var defaultName = names 57 | .FirstOrDefault(n => string.Equals(n, "Unknown", StringComparison.OrdinalIgnoreCase)); 58 | 59 | if (defaultName == null) 60 | { 61 | defaultName = names.First(); 62 | } 63 | 64 | return Enum.Parse(enumType, defaultName); 65 | } 66 | 67 | return null; 68 | } 69 | 70 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 71 | { 72 | writer.WriteValue(value.ToString()); 73 | } 74 | 75 | private static bool IsNullableType(Type t) 76 | { 77 | return (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)); 78 | } 79 | } 80 | 81 | static class EventCategoryExtensions 82 | { 83 | public static string GetJsonValue(this Enum @enum) 84 | { 85 | var fieldInfo = @enum.GetType().GetField(@enum.ToString()); 86 | 87 | return !(Attribute.GetCustomAttribute(fieldInfo, typeof(EnumMemberAttribute)) is EnumMemberAttribute attribute) 88 | ? @enum.ToString() 89 | : attribute.Value; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /wiremock/__files/app-discovery.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopes": "https://prod-global-auth.nubank.com.br/api/admin/scope", 3 | "creation": "https://prod-global-auth.nubank.com.br/api/creation", 4 | "change_password": "https://prod-global-auth.nubank.com.br/api/change-password", 5 | "smokejumper": "https://prod-cdn.nubank.com.br/mobile/fire-station/smokejumper.json", 6 | "block": "https://prod-global-auth.nubank.com.br/api/admin/block", 7 | "lift": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/lift", 8 | "shard_mapping_id": "https://prod-global-auth.nubank.com.br/api/mapping/:kind/:id", 9 | "force_reset_password": "https://prod-global-auth.nubank.com.br/api/admin/force-reset-password", 10 | "revoke_token": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revoke_token", 11 | "userinfo": "https://prod-global-auth.nubank.com.br/api/userinfo", 12 | "reset_password": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/reset_password", 13 | "unblock": "https://prod-global-auth.nubank.com.br/api/admin/unblock", 14 | "shard_mapping_cnpj": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/shard_mapping_cnpj", 15 | "shard_mapping_cpf": "https://prod-global-auth.nubank.com.br/api/mapping/cpf", 16 | "register_prospect": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_prospect", 17 | "engage": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/engage", 18 | "account_recovery_job": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/account_recovery_job", 19 | "account_recovery_confirm": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/account_recovery_confirm", 20 | "magnitude": "https://prod-s0-magnitude.nubank.com.br/api/events", 21 | "revoke_all": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revoke_all", 22 | "user_hypermedia": "https://prod-global-auth.nubank.com.br/api/admin/users/:id/hypermedia", 23 | "gen_certificate": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/gen_certificate", 24 | "email_verify": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/email_verify", 25 | "prospect_location": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/prospect_location", 26 | "token": "https://prod-global-auth.nubank.com.br/api/token", 27 | "account_recovery": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/account_recovery", 28 | "start_screen_v2": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/start_screen_v2", 29 | "scopes_remove": "https://prod-global-auth.nubank.com.br/api/admin/scope/:admin-id", 30 | "approved_products": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/approved_products", 31 | "admin_revoke_all": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/admin_revoke_all", 32 | "faq": { 33 | "ios": "https://ajuda.nubank.com.br/ios", 34 | "android": "https://ajuda.nubank.com.br/android", 35 | "wp": "https://ajuda.nubank.com.br/windows-phone" 36 | }, 37 | "scopes_add": "https://prod-global-auth.nubank.com.br/api/admin/scope/:admin-id", 38 | "registration": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/registration", 39 | "global_services": "https://prod-global-auth.nubank.com.br/api/mapping/global-services", 40 | "start_screen": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/start_screen", 41 | "user_change_password": "https://prod-global-auth.nubank.com.br/api/user/:user-id/password", 42 | "account_recovery_token": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/account_recovery_token", 43 | "user_status": "https://prod-global-auth.nubank.com.br/api/admin/user-status", 44 | "engage_and_create_credentials": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/engage_and_create_credentials" 45 | } -------------------------------------------------------------------------------- /src/NubankSharp/Entities/Transaction.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.Repositories.Api; 3 | using System; 4 | using System.Diagnostics; 5 | 6 | namespace NubankSharp.Entities 7 | { 8 | [DebuggerDisplay("{EventDate} {Name} {Number}/{Count} {Value}")] 9 | public class Transaction 10 | { 11 | public Guid Id { get; set; } 12 | public string Href { get; set; } 13 | public string Name { get; set; } 14 | public DateTime PostDate { get; set; } 15 | public DateTime EventDate { get; set; } 16 | public DateTime EventDateUtc { get; set; } 17 | public string Category { get; set; } 18 | public int Count { get; set; } 19 | public int Number { get; set; } 20 | public decimal Value { get; set; } 21 | public decimal Latitude { get; set; } 22 | public decimal Longitude { get; set; } 23 | public string Type { get; set; } 24 | 25 | // Propriedade da pagamento da fatura anterior 26 | public bool IsBillPaymentLastBill { get; set; } 27 | 28 | // Propriedade da pagamento da fatura anterior ou adiantamentos 29 | [JsonIgnore] 30 | public bool IsBillPayment => IsByllPayment(Name, Type); 31 | 32 | [JsonIgnore] 33 | public string CardName { get; set; } 34 | 35 | [JsonIgnore] 36 | public Statement Statement { get; set; } 37 | [JsonIgnore] 38 | 39 | public Guid Target { get; internal set; } 40 | [JsonIgnore] 41 | public Guid Origin { get; internal set; } 42 | [JsonIgnore] 43 | public bool IsCorrelated => Target != default || Origin != default; 44 | 45 | public Transaction() 46 | { 47 | } 48 | 49 | public Transaction(BillTransaction billTransaction) 50 | { 51 | Id = billTransaction.Id; 52 | Href = billTransaction.Event?.Links?.Self?.Href ?? billTransaction.Href; 53 | Category = billTransaction.Category; 54 | Count = billTransaction.Charges == 1 ? 0 : billTransaction.Charges; 55 | Number = billTransaction.Charges > 1 ? billTransaction.Index + 1 : 0; 56 | Name = billTransaction.Title; 57 | PostDate = billTransaction.PostDate; 58 | EventDate = billTransaction.EventDate; 59 | EventDateUtc = billTransaction.EventDateUtc; 60 | Latitude = billTransaction.Event?.Details?.Lat ?? 0; 61 | Longitude = billTransaction.Event?.Details?.Lon ?? 0; 62 | Value = (billTransaction.Amount / 100m) * -1; 63 | IsBillPaymentLastBill = billTransaction.IsBillPaymentLastBill; 64 | Type = Enum.GetName(typeof(TransactionType), TransactionType.CreditEvent); 65 | } 66 | 67 | public Transaction(SavingFeed saving) 68 | { 69 | Id = saving.Id; 70 | Href = null; 71 | Category = null; 72 | Count = 0; 73 | Number = 0; 74 | Name = saving.GetCompleteTitle(); 75 | PostDate = saving.PostDate; 76 | EventDate = saving.PostDate; 77 | EventDateUtc = saving.PostDate; 78 | Latitude = 0; 79 | Longitude = 0; 80 | Value = saving.GetValueWithSignal(); 81 | Type = Enum.GetName(typeof(TransactionType), saving.TypeName); 82 | IsBillPaymentLastBill = saving.TypeName == TransactionType.BillPaymentEvent; 83 | } 84 | 85 | public string GetNameFormatted() 86 | { 87 | if (Count > 0) 88 | return $"{Name} {Number}/{Count}"; 89 | 90 | return Name; 91 | } 92 | 93 | public static bool IsByllPayment(string name, string type) 94 | { 95 | return name == "Pagamento recebido" || type == "BillPaymentEvent"; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/TransactionExtensions.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp.DTOs; 2 | using NubankSharp.Entities; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace NubankSharp.Extensions 8 | { 9 | public static class TransactionExtensions 10 | { 11 | #region Statement 12 | 13 | public static IEnumerable GetTransactions(this IEnumerable statements) 14 | { 15 | return statements.SelectMany(f => f.Transactions); 16 | } 17 | 18 | public static List ExcludeBillPaymentLastBill(this List statements) 19 | { 20 | foreach (var s in statements) 21 | s.Transactions = s.Transactions.ExcludeBillPaymentLastBill().ToList(); 22 | 23 | return statements; 24 | } 25 | 26 | 27 | public static List ExcludeCorrelations(this List statements) 28 | { 29 | foreach (var s in statements) 30 | s.Transactions = s.Transactions.Where(f => !f.IsCorrelated).ToList(); 31 | 32 | return statements; 33 | } 34 | 35 | #endregion 36 | 37 | #region Summary 38 | 39 | public static SummaryDTO Summary(this IEnumerable transactions) 40 | { 41 | var tTn = transactions.GetIn(); 42 | var tOut = transactions.GetOut(); 43 | 44 | return new SummaryDTO 45 | { 46 | CountIn = tTn.Count(), 47 | ValueIn = tTn.Total(), 48 | 49 | CountOut = tOut.Count(), 50 | ValueOut = tOut.Total(), 51 | 52 | CountTotal = transactions.Count(), 53 | ValueTotal = transactions.Total(), 54 | }; 55 | } 56 | 57 | #endregion 58 | 59 | public static IEnumerable GetIn(this IEnumerable transactions) 60 | { 61 | return transactions.Where(f => f.Value > 0); 62 | } 63 | 64 | public static IEnumerable GetOut(this IEnumerable transactions) 65 | { 66 | return transactions.Where(f => f.Value < 0); 67 | } 68 | 69 | public static IEnumerable ExcludeBillPaymentLastBill(this IEnumerable transactions) 70 | { 71 | return transactions.Where(f => !f.IsBillPaymentLastBill); 72 | } 73 | 74 | public static void CorrelateTransactions(this IEnumerable transactions) 75 | { 76 | // Esse código precisa ser melhorado 77 | // 1) Primeiro, considere os débitos como sendo sempre a origin da relação (conta de crédit0) 78 | // 2) Considere os créditos como sendo sempre o destino origin da relação (conta do nuconta) 79 | // 3) Origin e destino devem ter o mesmo valor, mas com o sinal invertido 80 | // 4) Origin e destino não podem ser do mesmo cartão (um nuconta e outro crédito) 81 | // 5) As origins precisam ter sempre a data menor ou igual ao destino 82 | // 6) A diferença de data deve ser de no máximo 5 dias. Considere que o pagamento seja feita numa sexta e existe feriado prolongado 83 | // no qual a efetivação do pagamento da fatura seja só na terça feira (talvez não exista esse cenário no nubank) 84 | var groupCorrelation = transactions.GroupBy(f => new { f.PostDate, ValueAbs = Math.Abs(f.Value) }).Where(f => f.Count() == 2); 85 | foreach (var g in groupCorrelation) 86 | { 87 | var nuconta = g.FirstOrDefault(f => f.CardName == Card.NUCONTA_NAME); 88 | var creditCard = g.FirstOrDefault(f => f.CardName.Contains(Card.CREDIT_CARD_NAME)); 89 | 90 | if (nuconta != null && creditCard != null) 91 | { 92 | nuconta.Target = creditCard.Id; 93 | creditCard.Origin = nuconta.Id; 94 | } 95 | } 96 | } 97 | 98 | public static decimal Total(this IEnumerable transactions) 99 | { 100 | return transactions.Sum(f => f.Value); 101 | } 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Files/StatementFileRepository.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.DTOs; 3 | using NubankSharp.Entities; 4 | using NubankSharp.Extensions; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Linq.Dynamic.Core; 9 | 10 | namespace NubankSharp.Repositories.Files 11 | { 12 | public class StatementFileRepository 13 | { 14 | private JsonFileRepository _jsonFileRepository = new(); 15 | 16 | public IEnumerable GetStatements(string userPath, CardType? cardType = null, bool excludeBillPayment = false, bool excludeCorrelations = false, string where = null, string[] args = null, string orderby = null) 17 | { 18 | var list = new List(); 19 | var directories = Directory.GetDirectories(userPath); 20 | 21 | foreach (string d in directories) 22 | { 23 | var dirName = Path.GetFileName(d); 24 | 25 | // 1) Se for solicitado apenas cartão de credito e a pasta corrente NÃO for cartão de credito então continua 26 | // 2) Se for solicitado apenas NuConta e a pasta corrente NÃO for NuConta então continua 27 | if (cardType == CardType.CreditCard && !Card.CREDIT_CARD_NAME.Contains(dirName)) 28 | continue; 29 | else if (cardType == CardType.NuConta && !Card.NUCONTA_NAME.Contains(dirName)) 30 | continue; 31 | 32 | // Se a pasta atual for "credit-card-by-month" e existir a pasta "credit-card", então 33 | // dá prioridade para a pasta "credit-card" que é separada por boleto. Isso deixa a leitura do usuário 34 | // mais facil pois é igual ao que ele ver no APP 35 | if (dirName == Card.CREDIT_CARD_BY_MONTH_NAME && directories.Any(f => Path.GetFileName(f) == Card.CREDIT_CARD_NAME)) 36 | continue; 37 | 38 | var files = Directory.GetFiles(d); 39 | foreach (var f in files) 40 | { 41 | // var statement = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(f)); 42 | var statement = _jsonFileRepository.GetFile(f); 43 | 44 | foreach (var t in statement.Transactions) 45 | { 46 | t.CardName = statement.Card.Name; 47 | t.Statement = statement; 48 | } 49 | 50 | list.Add(statement); 51 | } 52 | } 53 | 54 | // Cria a correlação entre as transações 55 | // NuConta: manda 10 reais para o cartão de crédito 56 | // CreditCard: Recebe 10 reais 57 | // 1) A transação da NuConta terá na propriedade "Target" o Id da transação que foi para o cartão de crédito 58 | // 2) A transação do cartão de crédito terá na propriedade "Origin" o Id da transação que veio da NuConta 59 | list.GetTransactions().CorrelateTransactions(); 60 | 61 | if (excludeBillPayment) 62 | list = list.ExcludeBillPaymentLastBill(); 63 | 64 | if (excludeCorrelations) 65 | list = list.ExcludeCorrelations(); 66 | 67 | var queryable = list.AsQueryable(); 68 | 69 | if (!string.IsNullOrWhiteSpace(where)) 70 | queryable = queryable.Where(where, args); 71 | 72 | if (!string.IsNullOrWhiteSpace(orderby)) 73 | queryable = queryable.OrderBy(orderby); 74 | 75 | return queryable; 76 | } 77 | 78 | public IEnumerable GetTransactions(string userPath, string idOrName, string where = null, string[] args = null, string orderby = null) 79 | { 80 | var statements = GetStatements(userPath); 81 | var list = statements.GetTransactions(); 82 | 83 | var queryable = list.AsQueryable(); 84 | 85 | if (!string.IsNullOrWhiteSpace(idOrName)) 86 | queryable = queryable.Where($"Id.ToString().StartsWith(@0) || Name.StartsWith(@0)", idOrName); 87 | 88 | if (!string.IsNullOrWhiteSpace(where)) 89 | queryable = queryable.Where(where, args); 90 | 91 | if (!string.IsNullOrWhiteSpace(orderby)) 92 | queryable = queryable.OrderBy(orderby); 93 | 94 | return queryable; 95 | } 96 | 97 | public void Save(Statement e, string statementPath) 98 | { 99 | _jsonFileRepository.Save(e, statementPath); 100 | //FileExtensions.CreateFolderIfNeeded(statementPath); 101 | //File.WriteAllText(statementPath, JsonConvert.SerializeObject(e, Formatting.Indented)); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Formattters/FormatStringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | using Humanizer; 6 | 7 | namespace NubankSharp.Cli.Extensions.Formatters 8 | { 9 | public static class FormatStringExtensions 10 | { 11 | public const int MAX_LENGTH = 40; 12 | 13 | public static string Truncate(this string value, int length) 14 | => (value != null && value.Length > length) ? value.Substring(0, length) : value; 15 | 16 | public static string HumanizeDefault(this string text, int maxLength = MAX_LENGTH, string truncateIndicative = "...") 17 | { 18 | text = Regex.Replace(text, "(\n|\r|\t)+", " "); 19 | return text.Truncate(maxLength, truncateIndicative); 20 | } 21 | 22 | public static string TextIfNull(this T value, string textIfNull = "-") 23 | { 24 | if (value is null) 25 | return textIfNull; 26 | 27 | return value.ToString(); 28 | } 29 | 30 | public static string ToCamelCase(this string value) 31 | { 32 | return Char.ToLowerInvariant(value[0]) + value.Substring(1); 33 | } 34 | 35 | 36 | public static string ToLowerSeparate(this string str, char separate = '-') 37 | { 38 | if (!string.IsNullOrWhiteSpace(str)) 39 | { 40 | var newStr = ""; 41 | for (var i = 0; i < str.Length; i++) 42 | { 43 | var c = str[i]; 44 | if (i > 0 && separate != str[i - 1] && char.IsLetterOrDigit(str[i - 1]) && char.IsUpper(c) && !char.IsUpper(str[i - 1])) 45 | newStr += separate + c.ToString().ToLower(); 46 | else 47 | newStr += c.ToString().ToLower(); 48 | } 49 | 50 | return newStr; 51 | } 52 | 53 | return str; 54 | } 55 | 56 | public static string RemoveMoreThan2BreakLines(this string text) 57 | { 58 | text = Regex.Replace(text, $@"[{Environment.NewLine}]+", f => 59 | { 60 | var count = 0; 61 | 62 | foreach (var c in f.Value) 63 | { 64 | if (c == '\n') 65 | count++; 66 | } 67 | 68 | return count < 3 ? f.Value : Environment.NewLine + Environment.NewLine; 69 | }); 70 | return text; 71 | } 72 | 73 | public static string TrimFromPipe(this string text) 74 | { 75 | var strBuilder = new StringBuilder(); 76 | var lines = text.Split(Environment.NewLine); 77 | 78 | var startLinePos = 0; 79 | var addSpaces = 0; 80 | foreach (var line in lines) 81 | { 82 | var onlyPipe = line.Trim(); 83 | if (onlyPipe == "|") 84 | { 85 | startLinePos = line.IndexOf("|"); 86 | addSpaces = 0; 87 | continue; 88 | } 89 | 90 | if (onlyPipe == "||") 91 | { 92 | addSpaces = line.IndexOf("|") - startLinePos; 93 | startLinePos = line.IndexOf("|"); 94 | continue; 95 | } 96 | 97 | var countSpaces = 0; 98 | foreach (var c in line) 99 | { 100 | if (c == ' ') 101 | countSpaces++; 102 | else 103 | break; 104 | } 105 | 106 | string newLine; 107 | if (countSpaces >= startLinePos) 108 | { 109 | newLine = line.Length >= startLinePos ? line.Substring(startLinePos) : line; 110 | } 111 | else 112 | { 113 | newLine = line; 114 | } 115 | 116 | if (addSpaces > 0) 117 | newLine = newLine.AddSpacesInAllLines(addSpaces); 118 | 119 | if (newLine.EndsWith("\r\n")) 120 | strBuilder.Append(newLine); 121 | else 122 | strBuilder.AppendLine(newLine); 123 | } 124 | 125 | return strBuilder.ToString(); 126 | } 127 | 128 | public static string AddSpacesInAllLines(this string text, int spaces) 129 | { 130 | if (spaces == 0 || text?.Length == 0) 131 | return text; 132 | 133 | var strSpaces = new string(' ', spaces); 134 | var strBuilder = new StringBuilder(); 135 | var lines = Regex.Split(text, Environment.NewLine); 136 | 137 | foreach (var line in lines) 138 | strBuilder.AppendLine($"{strSpaces}{line}"); 139 | 140 | return strBuilder.ToString(); 141 | } 142 | 143 | public static string JoinIfNotNull(string separator, params string[] values) 144 | { 145 | return string.Join(separator, values.Where(f => f != null)); 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /src/NubankCli/Extensions/Environments/EnvironmentExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | 5 | namespace NubankSharp.Cli.Extensions 6 | { 7 | /// 8 | /// Helper class to working in debug mode 9 | /// 10 | public static class EnvironmentExtensions 11 | { 12 | private static string projectDirectory; 13 | private static object thisLock = new object(); 14 | private static string executionPath; 15 | 16 | /// 17 | /// Check if is in debug mode 18 | /// 19 | public static bool IsAttached 20 | { 21 | get 22 | { 23 | return System.Diagnostics.Debugger.IsAttached; 24 | } 25 | } 26 | 27 | public static string DebugOrExecutionDirectory 28 | { 29 | get 30 | { 31 | if (IsAttached) 32 | return GetProjectDirectory(); 33 | 34 | return ExecutionDirectory; 35 | } 36 | } 37 | 38 | public static string ProjectRootOrExecutionDirectory 39 | { 40 | get 41 | { 42 | #if RELEASE 43 | return Path.GetDirectoryName(Environment.GetCommandLineArgs()[0]); 44 | #else 45 | 46 | var projectRoot = GetProjectDirectory(); 47 | return projectRoot; 48 | #endif 49 | } 50 | } 51 | 52 | public static string ExecutionDirectory 53 | { 54 | get 55 | { 56 | if (executionPath == null) 57 | executionPath = new FileInfo(Assembly.GetEntryAssembly().Location).Directory.FullName; 58 | 59 | return executionPath; 60 | } 61 | } 62 | 63 | /// 64 | /// Get the project base path 65 | /// 66 | /// Current directory, if null get from de system 67 | /// Project base path 68 | public static string GetProjectDirectory(string baseDir = null) 69 | { 70 | lock (thisLock) 71 | { 72 | if (projectDirectory == null) 73 | { 74 | var pathFull = baseDir ?? new FileInfo(Assembly.GetEntryAssembly().Location).Directory.FullName; 75 | projectDirectory = pathFull; 76 | 77 | // if TRUE is because in VisualStudio 78 | var i = 1; 79 | do 80 | { 81 | if (Directory.GetFiles(projectDirectory, "project.json").Length != 0) 82 | return projectDirectory; 83 | else if (Directory.GetFiles(projectDirectory, "*.csproj").Length != 0) 84 | return projectDirectory; 85 | else if (Directory.GetFiles(projectDirectory, "*.xproj").Length != 0) 86 | return projectDirectory; 87 | projectDirectory = GetHigherDirectoryPath(pathFull, i); 88 | i++; 89 | } 90 | while (projectDirectory != null); 91 | 92 | throw new System.Exception("No project files were found"); 93 | } 94 | } 95 | 96 | return projectDirectory; 97 | } 98 | 99 | public static string GetResourceContent(string resourcePath) 100 | { 101 | // Tenta encontrar nas pastas fisicas 102 | // 1) Tenta na pasta raiz do projeto (onde está o csproj) 103 | // 2) Se não encontrar (modo release), então tenta encontrar na pasta de execução (bin) 104 | // 2.1) Nesse caso o arquivo razor deve estar configurado para sempre ser copiado para a output 105 | // 3) Se não encontrar em nenhum dos locais, então tenta no embededResources 106 | // 3.1) Nesse caso o arquivo razor deve estar configurado para sempre ser compilado junto ao assembly 107 | var fullPath = Path.Combine(GetProjectDirectory(), resourcePath); 108 | 109 | if (!File.Exists(fullPath)) 110 | fullPath = Path.Combine(ExecutionDirectory, resourcePath); 111 | 112 | if (File.Exists(fullPath)) 113 | return File.ReadAllText(fullPath); 114 | 115 | resourcePath = resourcePath.Replace("/", ".").ToLower(); 116 | var assembly = Assembly.GetEntryAssembly(); 117 | foreach (var f in assembly.GetManifestResourceNames()) 118 | { 119 | if (f.ToLower().Contains(resourcePath)) 120 | { 121 | using Stream stream = assembly.GetManifestResourceStream(f); 122 | using StreamReader reader = new StreamReader(stream); 123 | return reader.ReadToEnd(); 124 | } 125 | } 126 | 127 | return null; 128 | } 129 | 130 | private static string GetHigherDirectoryPath(string srcPath, int upLevel) 131 | { 132 | string[] directoryElements = srcPath.Split(Path.DirectorySeparatorChar); 133 | if (upLevel >= directoryElements.Length) 134 | { 135 | return null; 136 | } 137 | else 138 | { 139 | string[] resultDirectoryElements = new string[directoryElements.Length - upLevel]; 140 | for (int elementIndex = 0; elementIndex < resultDirectoryElements.Length; elementIndex++) 141 | { 142 | resultDirectoryElements[elementIndex] = directoryElements[elementIndex]; 143 | } 144 | return string.Join(Path.DirectorySeparatorChar.ToString(), resultDirectoryElements); 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /src/NubankCli/Commands/Statements/GetCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Commands.Statements 2 | { 3 | using SysCommand.ConsoleApp; 4 | using SysCommand.Mapping; 5 | using System; 6 | using NubankSharp.Extensions; 7 | using System.Linq; 8 | using System.Linq.Dynamic.Core; 9 | using NubankSharp.Cli.Extensions.Formatters; 10 | using NubankSharp; 11 | using NubankSharp.Services; 12 | using NubankSharp.Entities; 13 | using System.Collections.Generic; 14 | using NubankSharp.Repositories.Files; 15 | 16 | public partial class StatementCommand : Command 17 | { 18 | 19 | [Action(Name = "get")] 20 | public void Get( 21 | EntityNames type, 22 | [Argument(ShortName = 'c', LongName = "card")] CardType? card = null, 23 | [Argument(ShortName = 'm', LongName = "merge")] bool merge = false, 24 | [Argument(ShortName = 'h', LongName = "by-month")] bool byMonth = false, 25 | [Argument(ShortName = 'e', LongName = "exclude-redundance")] bool excludeRedundance = true, 26 | [Argument(ShortName = 'w', LongName = "where")] string where = null, 27 | [Argument(ShortName = 's', LongName = "sort")] string sort = "Start ASC", 28 | [Argument(ShortName = 'P', LongName = "page")] int page = CommandExtensions.FIRST_PAGE, 29 | [Argument(ShortName = 'S', LongName = "page-size")] int pageSize = 100, 30 | [Argument(ShortName = 'A', LongName = "auto-page")] bool autoPage = true, 31 | [Argument(ShortName = 'o', LongName = "output")] string outputFormat = null 32 | ) 33 | { 34 | try 35 | { 36 | var statementFileRepository = new StatementFileRepository(); 37 | IEnumerable statements; 38 | 39 | // 1) Quando for cartão de credito, então removemos os pagamentos por padrão para que a visualização fique igual ao valor do boleto do APP 40 | // 2) Quando for nuConta, então mantemos entradas e saídas para o saldo ser o mais próximo possível da APP 41 | // 3) Quando for NULL (ambos), então removemos as transações correlacionadas, ou seja, transações que saíram da nuConta e entraram no cartão de crédito 42 | // Isso deve ser feito pois do contrário teremos o saldo de entrada e saída duplicados: 43 | // NuConta: 44 | // 1) Entrou 100 reais do banco X para pagar o boleto 45 | // 2) Saiu 100 da nuConta para ir para o crédito 46 | // ENTRADA: 100 47 | // SAÍDA: 100 48 | // SALDO: 0 49 | // Crédito: 50 | // 1) Entrou 100 reais para pagar o boleto 51 | // 2) A somatória das contas equivalem a 100 reais. 52 | // ENTRADA: 100 53 | // SAÍDA: 100 54 | // SALDO: 0 55 | // Total: 56 | // 1) É feito a somatória dos dois cartões e ai temos o problema da duplicação da ENTRADA e SAÍDA 57 | // ENTRADA: 200 58 | // SAÍDA: 200 59 | // SALDO: 0 60 | var userPath = this.GetUserPath(this.GetCurrentUser()); 61 | if (card == CardType.CreditCard) 62 | statements = statementFileRepository.GetStatements(userPath, card, excludeRedundance, false, where, null, sort); 63 | else if (card == CardType.NuConta) 64 | statements = statementFileRepository.GetStatements(userPath, card, false, false, where, null, sort); 65 | else 66 | statements = statementFileRepository.GetStatements(userPath, card, false, excludeRedundance, where, null, sort); 67 | 68 | if (byMonth || merge) 69 | { 70 | statements = StatementExtensions.ToStatementByMonth(statements.GetTransactions(), merge); 71 | } 72 | 73 | var summary = statements.GetTransactions().Summary(); 74 | 75 | App.Console.Success($" "); 76 | App.Console.Success($"TOTAL (ENTRADA): " + $"{summary.ValueIn.Format()} ({summary.CountIn})"); 77 | App.Console.Success($"TOTAL (SAÍDA) : " + $"{summary.ValueOut.Format()} ({summary.CountOut})"); 78 | App.Console.Success($"TOTAL : " + $"{summary.ValueTotal.Format()} ({summary.CountTotal})"); 79 | App.Console.Success($" "); 80 | 81 | if (page <= 0 || pageSize <= 0) 82 | { 83 | this.ViewFormatted(statements, outputFormat); 84 | } 85 | else 86 | { 87 | this.ViewPagination(page, currentPage => 88 | { 89 | var pageResult = statements.AsQueryable().PageResult(currentPage, pageSize); 90 | return pageResult; 91 | }, autoPage, outputFormat); 92 | } 93 | 94 | if (card == CardType.CreditCard) 95 | { 96 | App.Console.Warning($" "); 97 | App.Console.Warning("Transações do tipo 'Pagamento Recebido' não estão sendo consideradas nas somatórias. Para inclui-las utilize '--exclude-redundance false' ou '-e false'"); 98 | } 99 | else if (card == null) 100 | { 101 | App.Console.Warning($" "); 102 | App.Console.Warning("Transações correlacionadas entre a NuConta e o cartão de crédito foram removidas para que os saldos de entrada e saídas não sejam duplicados"); 103 | } 104 | } 105 | catch (Exception ex) 106 | { 107 | this.ShowApiException(ex); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/NubankSharp/Extensions/StatementExtensions.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp.Entities; 2 | using NubankSharp.Extensions; 3 | using NubankSharp.Repositories.Api; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace NubankSharp.Extensions 8 | { 9 | public static class StatementExtensions 10 | { 11 | public static List ToStatementByBill(this IEnumerable bills, Card card) 12 | { 13 | var list = new List(); 14 | foreach (var b in bills) 15 | { 16 | var statement = new Statement 17 | { 18 | Version = Statement.Version1_0, 19 | StatementType = StatementType.ByBill, 20 | Start = b.Summary.OpenDate, 21 | End = b.Summary.CloseDate, 22 | Card = card, 23 | Transactions = new List() 24 | }; 25 | 26 | foreach (var l in b.LineItems) 27 | statement.Transactions.Add(new Transaction(l)); 28 | 29 | list.Add(statement); 30 | } 31 | 32 | return list; 33 | } 34 | 35 | // VALIDAR 36 | //public static List ToStatementByMonth(this IEnumerable bills, Card card) 37 | //{ 38 | // var months = bills.GroupBy(f => f.EventDate.GetDateBeginningOfMonth()).Select(f => 39 | // { 40 | // return new 41 | // { 42 | // DateBegginingOfMonth = f.Key, 43 | // Transactions = f.ToList() 44 | // }; 45 | // }).ToList(); 46 | 47 | // var list = new List(); 48 | // foreach (var m in months) 49 | // { 50 | // var statement = new Statement 51 | // { 52 | // Version = Statement.Version1_0, 53 | // StatementType = StatementType.ByMonth, 54 | // Start = m.DateBegginingOfMonth, 55 | // End = m.DateBegginingOfMonth.GetDateEndOfMonth(), 56 | // Card = card, 57 | // Transactions = new List() 58 | // }; 59 | 60 | // foreach (var l in m.Transactions) 61 | // statement.Transactions.Add(new Transaction(l)); 62 | 63 | // statement.Transactions = statement.Transactions.OrderBy(f => f.EventDate).ToList(); 64 | // list.Add(statement); 65 | // } 66 | 67 | // return list; 68 | //} 69 | 70 | public static List ToStatementByMonth(this IEnumerable transactions, bool mergeCards = false) 71 | { 72 | var months = transactions.GroupBy(f => new { CardName = mergeCards ? "" : f.Statement.Card.Name, Date = f.EventDate.GetDateBeginningOfMonth() }).Select(f => 73 | { 74 | return new 75 | { 76 | Card = mergeCards ? null : f.First().Statement.Card, 77 | DateBegginingOfMonth = f.Key.Date, 78 | Transactions = f.ToList() 79 | }; 80 | }).ToList(); 81 | 82 | var list = new List(); 83 | foreach (var m in months) 84 | { 85 | var statement = new Statement 86 | { 87 | Version = Statement.Version1_0, 88 | StatementType = StatementType.ByMonth, 89 | Start = m.DateBegginingOfMonth, 90 | End = m.DateBegginingOfMonth.GetDateEndOfMonth(), 91 | Card = m.Card, 92 | Transactions = new List() 93 | }; 94 | 95 | foreach (var l in m.Transactions) 96 | statement.Transactions.Add(new Transaction 97 | { 98 | Id = l.Id, 99 | CardName = l.CardName, 100 | Category = l.Category, 101 | Count = l.Count, 102 | EventDate = l.EventDate, 103 | EventDateUtc = l.EventDateUtc, 104 | Href = l.Href, 105 | Latitude = l.Latitude, 106 | Longitude = l.Longitude, 107 | Name = l.Name, 108 | Number = l.Number, 109 | PostDate = l.PostDate, 110 | Statement = statement, 111 | Value = l.Value 112 | }); 113 | 114 | statement.Transactions = statement.Transactions.OrderBy(f => f.EventDate).ToList(); 115 | list.Add(statement); 116 | } 117 | 118 | return list; 119 | } 120 | 121 | public static List ToStatementByMonth(this IEnumerable transactions, Card card = null, bool sortByPostDate = true) 122 | { 123 | var months = transactions.GroupBy(f => (sortByPostDate ? f.PostDate : f.EventDate).GetDateBeginningOfMonth()).Select(f => 124 | { 125 | return new 126 | { 127 | DateBegginingOfMonth = f.Key, 128 | Transactions = f.ToList() 129 | }; 130 | }).ToList(); 131 | 132 | var list = new List(); 133 | foreach (var m in months) 134 | { 135 | var statement = new Statement 136 | { 137 | Version = Statement.Version1_0, 138 | StatementType = StatementType.ByMonth, 139 | Start = m.DateBegginingOfMonth, 140 | End = m.DateBegginingOfMonth.GetDateEndOfMonth(), 141 | Card = card, 142 | Transactions = m.Transactions 143 | }; 144 | 145 | statement.Transactions = statement.Transactions.OrderBy(f => f.EventDate).ToList(); 146 | list.Add(statement); 147 | } 148 | 149 | return list; 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /wiremock/__files/LoginAccessTokenTwoFactory.json.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.4Adcj3UFYzPUVaVF43FmMab6RlaQD8A9V8wFzzht-KQ", 3 | "token_type": "bearer", 4 | "_links": { 5 | "rewards_customer_enrollment": { 6 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/rewards_customer_enrollment" 7 | }, 8 | "rosetta_images": { 9 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/rosetta_images" 10 | }, 11 | "change_password": { 12 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/change_password" 13 | }, 14 | "enabled_features": { 15 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/enabled_features" 16 | }, 17 | "insurance_bff": { 18 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/insurance_bff" 19 | }, 20 | "certificate_status": { 21 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/certificate_status" 22 | }, 23 | "telefonista": { 24 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/telefonista" 25 | }, 26 | "rosetta_localization": { 27 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/rosetta_localization" 28 | }, 29 | "credit_card_widget": { 30 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/credit_card_widget" 31 | }, 32 | "revoke_token": { 33 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revoke_token" 34 | }, 35 | "userinfo": { 36 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/userinfo" 37 | }, 38 | "events_page": { 39 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/events_page" 40 | }, 41 | "loginWebapp": { 42 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/loginWebapp" 43 | }, 44 | "dropman": { 45 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/dropman" 46 | }, 47 | "token_validate": { 48 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/token_validate" 49 | }, 50 | "events": { 51 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/events" 52 | }, 53 | "register_rewards": { 54 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/register_rewards" 55 | }, 56 | "login_webapp": { 57 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/login_webapp" 58 | }, 59 | "postcode": { 60 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/postcode" 61 | }, 62 | "app_flows": { 63 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/app_flows" 64 | }, 65 | "magnitude": { 66 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/magnitude" 67 | }, 68 | "facade": { 69 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/facade" 70 | }, 71 | "rewards_signup_widget": { 72 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/rewards_signup_widget" 73 | }, 74 | "revoke_all": { 75 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revoke_all" 76 | }, 77 | "customer": { 78 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/customer" 79 | }, 80 | "account": { 81 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/account" 82 | }, 83 | "bills_summary": { 84 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/bills_summary" 85 | }, 86 | "start_auto_trust": { 87 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/start_auto_trust" 88 | }, 89 | "features_map": { 90 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/features_map" 91 | }, 92 | "healthcheck": { 93 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/healthcheck" 94 | }, 95 | "savings_account": { 96 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/savings_account" 97 | }, 98 | "selfie_authorization": { 99 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/selfie_authorization" 100 | }, 101 | "rewards_enrollment": { 102 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/rewards_enrollment" 103 | }, 104 | "purchases": { 105 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/purchases" 106 | }, 107 | "blackmirror": { 108 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/blackmirror" 109 | }, 110 | "ghostflame": { 111 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/nuconta" 112 | }, 113 | "revelio": { 114 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/revelio" 115 | }, 116 | "canuto": { 117 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/canuto" 118 | }, 119 | "shore": { 120 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/shore" 121 | }, 122 | "user_change_password": { 123 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/user_change_password" 124 | }, 125 | "rosetta_localizations_by_locale": { 126 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/rosetta_localizations_by_locale" 127 | }, 128 | "tolkien_registry": { 129 | "href": "https://prod-global-webapp-proxy.nubank.com.br/api/proxy/tolkien_registry" 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Model/Savings/SavingFeed.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.Entities; 3 | using NubankSharp.Extensions; 4 | using System; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace NubankSharp.Repositories.Api 10 | { 11 | [DebuggerDisplay("{PostDate} - {Title} - {Amount} - {TypeName}")] 12 | public class SavingFeed 13 | { 14 | public Guid Id { get; set; } 15 | 16 | [JsonProperty("__typename")] 17 | [JsonConverter(typeof(TolerantEnumConverter))] 18 | public TransactionType TypeName { get; set; } 19 | 20 | public string Title { get; set; } 21 | public string Detail { get; set; } 22 | public DateTime PostDate { get; set; } 23 | public decimal? Amount { get; set; } 24 | public Account OriginAccount { get; set; } 25 | public Account DestinationAccount { get; set; } 26 | 27 | public decimal? GetValueFromDetails() 28 | { 29 | var match = Regex.Match(Detail, @"R\$\s*[-\d.,]+"); 30 | 31 | if (match.Success) 32 | { 33 | string matchValue = match.Value; 34 | string valueStr = matchValue.Substring(2).Trim(); 35 | return DecimalExtensions.ParseFromPtBr(valueStr).Value; 36 | } 37 | 38 | return null; 39 | } 40 | 41 | public string[] SplitDetails() 42 | { 43 | return Detail.Split(new char[] { '-', '\n' }); 44 | } 45 | 46 | public decimal GetValueWithSignal() 47 | { 48 | return TypeName switch 49 | { 50 | TransactionType.TransferInEvent => Amount.Value, 51 | TransactionType.TransferOutEvent => (Amount ?? 0) * -1, 52 | TransactionType.BarcodePaymentEvent => (Amount ?? 0) * -1, 53 | TransactionType.BarcodePaymentFailureEvent => Amount.Value, 54 | TransactionType.DebitPurchaseEvent => (Amount ?? 0) * -1, 55 | TransactionType.DebitPurchaseReversalEvent => Amount.Value, 56 | TransactionType.BillPaymentEvent => (Amount ?? 0) * -1, 57 | TransactionType.CanceledScheduledTransferOutEvent => ThrowNotImplementedException(), 58 | TransactionType.AddToReserveEvent => ThrowNotImplementedException(), 59 | TransactionType.CanceledScheduledBarcodePaymentRequestEvent => ThrowNotImplementedException(), 60 | TransactionType.RemoveFromReserveEvent => ThrowNotImplementedException(), 61 | TransactionType.TransferOutReversalEvent => (Amount ?? 0) * -1, 62 | TransactionType.SalaryPortabilityRequestEvent => ThrowNotImplementedException(), 63 | TransactionType.SalaryPortabilityRequestApprovalEvent => ThrowNotImplementedException(), 64 | TransactionType.DebitWithdrawalFeeEvent => ThrowNotImplementedException(), 65 | TransactionType.DebitWithdrawalEvent => Amount.Value, 66 | TransactionType.GenericFeedEvent => Title?.ToLower().Contains("transferência recebida") == true ? Amount ?? 0 : (Amount ?? 0) * -1, 67 | TransactionType.LendingTransferInEvent => Amount ?? 0, 68 | TransactionType.LendingTransferOutEvent => (Amount ?? 0) * -1, 69 | TransactionType.Unknown => 0, 70 | TransactionType.WelcomeEvent => 0, 71 | _ => ThrowNotImplementedException(), 72 | }; 73 | } 74 | 75 | public string GetCompleteTitle() 76 | { 77 | var detailsFirstValue = SplitDetails().FirstOrDefault().Trim(); 78 | var separator = " - "; 79 | 80 | return TypeName switch 81 | { 82 | TransactionType.TransferInEvent => JoinIfNotNull(separator, Title, OriginAccount?.Name), 83 | TransactionType.TransferOutEvent => JoinIfNotNull(separator, Title, DestinationAccount?.Name), 84 | TransactionType.BarcodePaymentEvent => $"{Detail}", 85 | TransactionType.BarcodePaymentFailureEvent => JoinIfNotNull(separator, Title, Detail), 86 | TransactionType.DebitPurchaseEvent => $"{detailsFirstValue}", 87 | TransactionType.DebitPurchaseReversalEvent => JoinIfNotNull(separator, Title, Detail), 88 | TransactionType.BillPaymentEvent => JoinIfNotNull(separator, Title, "Cartão Nubank"), 89 | TransactionType.CanceledScheduledTransferOutEvent => ThrowNotImplementedException(), 90 | TransactionType.AddToReserveEvent => ThrowNotImplementedException(), 91 | TransactionType.CanceledScheduledBarcodePaymentRequestEvent => ThrowNotImplementedException(), 92 | TransactionType.RemoveFromReserveEvent => ThrowNotImplementedException(), 93 | TransactionType.TransferOutReversalEvent => JoinIfNotNull(separator, Title, detailsFirstValue), 94 | TransactionType.SalaryPortabilityRequestEvent => ThrowNotImplementedException(), 95 | TransactionType.SalaryPortabilityRequestApprovalEvent => ThrowNotImplementedException(), 96 | TransactionType.DebitWithdrawalFeeEvent => ThrowNotImplementedException(), 97 | TransactionType.DebitWithdrawalEvent => detailsFirstValue, 98 | TransactionType.GenericFeedEvent => JoinIfNotNull(separator, Title, detailsFirstValue), 99 | TransactionType.LendingTransferInEvent => Title, 100 | TransactionType.LendingTransferOutEvent => Title, 101 | TransactionType.Unknown => null, 102 | TransactionType.WelcomeEvent => null, 103 | _ => ThrowNotImplementedException(), 104 | }; 105 | } 106 | 107 | private T ThrowNotImplementedException() 108 | { 109 | throw new NotImplementedException($"Não foi encontrado um mapeamento para o tipo '{TypeName}' que foi encontrado na transação '{Title} ({Detail})'"); 110 | } 111 | 112 | public static string JoinIfNotNull(string separator, params string[] values) 113 | { 114 | return string.Join(separator, values.Where(f => f != null)); 115 | } 116 | } 117 | 118 | public class Account 119 | { 120 | public string Name { get; set; } 121 | } 122 | 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/Services/BillService.cs: -------------------------------------------------------------------------------- 1 | using NubankSharp.Entities; 2 | using NubankSharp.Extensions; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace NubankSharp.Repositories.Api.Services 8 | { 9 | internal class BillService 10 | { 11 | private readonly NuApi client; 12 | private IEnumerable _bills; 13 | 14 | private IEnumerable Bills 15 | { 16 | get 17 | { 18 | if (_bills == null) 19 | _bills = client.GetBills(); 20 | 21 | return _bills; 22 | } 23 | } 24 | 25 | public BillService(NuApi nuApi) 26 | { 27 | this.client = nuApi; 28 | } 29 | 30 | public IEnumerable GetBillTransactions(DateTime? start, DateTime? end) 31 | { 32 | start = (start ?? Bills.Min(f => f.Summary.OpenDate)).Date.GetDateBeginningOfMonth(); 33 | end = (end ?? Bills.Max(f => f.Summary.CloseDate)).Date.GetDateBeginningOfMonth(); 34 | 35 | /* 36 | Eu quero todas as transações do mês 2020-07 até 2020-09 considerando que o dia de corte é 17 37 | 38 | Boleto 2020-06 (Open=2020-06-17, Close=2020-07-17) 39 | Aqui você encontra transações do mês 6 e 7 40 | O mês 6 vai de 2020-06-17 até 2020-06-30 41 | O mês 7 vai de 2020-07-01 até 2020-07-17 42 | Boleto 2020-07 (Open=2020-07-17, Close=2020-08-17) 43 | Aqui você encontra transações do mês 7 e 8 44 | O mês 7 vai de 2020-07-17 até 2020-07-31 45 | O mês 8 vai de 2020-08-01 até 2020-08-17 46 | Boleto 2020-08 (Open=2020-08-17, Close=2020-09-17) 47 | Aqui você encontra transações do mês 8 e 9 48 | O mês 8 vai de 2020-08-17 até 2020-08-31 49 | O mês 9 vai de 2020-09-01 até 2020-09-17 50 | Boleto 2020-09 (Open=2020-09-17, Close=2020-10-17) 51 | Aqui você encontra transações do mês 9 e 10 52 | O mês 9 vai de 2020-09-17 até 2020-09-31 53 | O mês 10 vai de 2020-10-01 até 2020-10-17 54 | 55 | -> Regra de seleção dos boletos: 56 | 57 | Boletos 2020-06 2020-09 58 | Close_Date = 2020-07-17 && Open_Date = 2020-09-17 59 | 01..17 17..31 60 | */ 61 | 62 | var transactions = new List(); 63 | var selecteds = Bills 64 | .Where(f => f.Summary.CloseDate.GetDateBeginningOfMonth() >= start && f.Summary.OpenDate.GetDateBeginningOfMonth() <= end) 65 | .OrderBy(f => f.Summary.OpenDate) 66 | .ToList(); 67 | 68 | foreach (var b in selecteds) 69 | { 70 | var response = client.GetBill($"{b.Summary.OpenDate:yy-MM-dd}", b.Links.Self.Href); 71 | if (response != null) 72 | { 73 | b.LineItems = response.LineItems; 74 | transactions.AddRange(response.LineItems); 75 | } 76 | } 77 | 78 | var selectedsTrans = transactions.Where(f => f.PostDate.GetDateBeginningOfMonth() >= start && f.PostDate.GetDateBeginningOfMonth() <= end).ToList(); 79 | return selectedsTrans; 80 | } 81 | 82 | public IEnumerable GetBills(DateTime? start, DateTime? end, bool ignoreFuture = true, bool includeItems = true) 83 | { 84 | start = (start ?? Bills.Min(f => f.Summary.OpenDate)).Date.GetDateBeginningOfMonth(); 85 | end = (end ?? Bills.Max(f => f.Summary.CloseDate)).Date.GetDateBeginningOfMonth(); 86 | 87 | // f.Links?.Self?.Href != null: As vezes não vem com link, isso ocorre muitas vezes em faturas futuras 88 | var selecteds = Bills 89 | .Where(f => (ignoreFuture && f.Links?.Self?.Href != null) && f.Summary.OpenDate.GetDateBeginningOfMonth() >= start && f.Summary.OpenDate.GetDateBeginningOfMonth() <= end) 90 | .OrderBy(f => f.Summary.OpenDate) 91 | .ToList(); 92 | 93 | if (includeItems) 94 | IncludeItems(selecteds); 95 | 96 | return selecteds; 97 | } 98 | 99 | private void IncludeItems(IEnumerable bills) 100 | { 101 | foreach (var b in bills) 102 | { 103 | var response = client.GetBill($"{b.Summary.OpenDate:yy-MM-dd}", b.Links.Self.Href); 104 | if (response != null) 105 | b.LineItems = response.LineItems; 106 | } 107 | } 108 | 109 | public void PopulateEvents(IEnumerable bills, IEnumerable events) 110 | { 111 | foreach (var b in bills) 112 | PopulateEvents(b.LineItems, events); 113 | } 114 | 115 | public void PopulateEvents(IEnumerable billTransactions, IEnumerable events) 116 | { 117 | if (billTransactions == null || events == null) 118 | return; 119 | 120 | // Adiciona uma nova flag para boletos que tem adiantamento de fatura 121 | // Onde o item mais antigo de "Pagamento recebido" será considerado o pagamento da fatura anterior 122 | // As demais serão consideradas adiantamentos 123 | var billPaymentLastBill = billTransactions.Where(f => Transaction.IsByllPayment(f.Title, null)).OrderBy(f => f.EventDate).FirstOrDefault(); 124 | if (billPaymentLastBill != null) 125 | billPaymentLastBill.IsBillPaymentLastBill = true; 126 | 127 | // Obtem as informações do evento de cada compra 128 | // Pagamentos recebidos e Compras do Rewards não tem links, então não existe referencia dentro do eventos 129 | // Compras com mais de uma parcela vão compartilhar o mesmo evento uma vez que o nubank só mantem o evento da primeira compra 130 | // mas é bom as outras parcelas terem a referencia do mesmo evento para ter acesso a latitude e longitude, mas devem tomar 131 | // cuidado para não usar a mesma data, pois apenas a primeira compra reflete a data real. 132 | foreach (var t in billTransactions) 133 | { 134 | if (t.Href != null) 135 | { 136 | // Algumas compras não tem evento relacionado, por exemplo: contas parceladas 137 | t.Event = events.FirstOrDefault(f => f.Href == t.Href); 138 | } 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ -------------------------------------------------------------------------------- /src/NubankCli/Commands/Auth/AuthCommand.cs: -------------------------------------------------------------------------------- 1 | using SysCommand.ConsoleApp; 2 | using System; 3 | using NubankSharp.Repositories.Api; 4 | using NubankSharp.Extensions; 5 | using Microsoft.Extensions.Options; 6 | using SysCommand.Mapping; 7 | using QRCoder; 8 | using NubankSharp.Entities; 9 | using NubankSharp.Repositories.Files; 10 | using NubankSharp.Models; 11 | 12 | namespace NubankSharp.Cli 13 | { 14 | public class AuthCommand : Command 15 | { 16 | private NuAppSettings _appSettings; 17 | private NuAppSettings AppSettings 18 | { 19 | get 20 | { 21 | if (_appSettings == null) 22 | _appSettings = this.GetService>().Value; 23 | 24 | return _appSettings; 25 | } 26 | } 27 | 28 | public void Login(string userName, string password = null) 29 | { 30 | try 31 | { 32 | if (string.IsNullOrWhiteSpace(userName)) 33 | userName = App.Console.Read("UserName: "); 34 | 35 | ValidateInfo(userName, nameof(userName)); 36 | 37 | var currentUser = this.GetUser(userName); 38 | 39 | if (currentUser != null 40 | && !currentUser.IsValid() 41 | && !string.IsNullOrWhiteSpace(currentUser.Token) 42 | && !string.IsNullOrWhiteSpace(currentUser.RefreshToken) 43 | ) 44 | { 45 | var httpClient = this.CreateNuHttpClient(currentUser, nameof(Login)); 46 | var endPointRepository = new EndPointApi(httpClient); 47 | var authRepository = new NuAuthApi(httpClient, endPointRepository); 48 | 49 | try 50 | { 51 | App.Console.Write($"Iniciando refresh token..."); 52 | authRepository.RefreshToken(); 53 | App.Console.Success($"Token atualizado com sucesso!"); 54 | currentUser.CleanPassword(); 55 | this.SaveUser(currentUser); 56 | } 57 | catch (Exception ex) 58 | { 59 | App.Console.Warning($"Ocorreu um erro na tentativa de fazer o refresh token, será iniciado o login novamente:" + ex.Message); 60 | currentUser = null; 61 | } 62 | } 63 | 64 | if (currentUser == null || !currentUser.IsValid()) 65 | { 66 | currentUser = new NuUser(userName, password); 67 | 68 | var httpClient = this.CreateNuHttpClient(currentUser, nameof(Login)); 69 | var endPointRepository = new EndPointApi(httpClient); 70 | var authRepository = new NuAuthApi(httpClient, endPointRepository); 71 | 72 | // PARTE_1: Cria a aplicação no nubank e requisita o código por e-mail 73 | App.Console.Write($"Criando contexto de login..."); 74 | var connectAppRes = authRepository.ConnectApp(userName, password); 75 | App.Console.Success($"Contexto criado com sucesso! Um e-mail será enviado para {connectAppRes.SentTo} contendo o código de acesso"); 76 | 77 | // PARTE_2: Valida o código e gera o certificado .p12 78 | var code = App.Console.Read($"[>] Digite o código: "); 79 | App.Console.Write($"Validando código...'"); 80 | authRepository.GenerateAppCertificate(connectAppRes, code); 81 | App.Console.Success($"Código validado com sucesso!"); 82 | 83 | // PARTE_3: Faz o login e obtem as infos do usuário com o token e links 84 | App.Console.Write($"Iniciando login do usuário...'"); 85 | authRepository.Login(); 86 | 87 | currentUser.CleanPassword(); 88 | this.SaveUser(currentUser); 89 | 90 | var repo = new NuApi(httpClient, endPointRepository); 91 | var debits = repo.GetDebitTransactions(); 92 | } 93 | 94 | this.SetCurrentUser(userName); 95 | 96 | App.Console.Success($"Sucesso! Login efetuado com '{currentUser.GetLoginType()}'"); 97 | App.Console.Write($" "); 98 | App.Console.Write($"LOCALIZAÇÃO USUÁRIO:"); 99 | App.Console.Write($" {this.GetUserFileName(currentUser)}"); 100 | App.Console.Write($" "); 101 | App.Console.Warning($"Seu token expira em: {currentUser.GetExpiredDate()}"); 102 | } 103 | catch (Exception ex) 104 | { 105 | this.ShowApiException(ex); 106 | } 107 | } 108 | 109 | public void Login( 110 | [Argument(ShortName = 'q', LongName = "qrcode")] bool qrCode, 111 | string userName, 112 | string password = null 113 | ) 114 | { 115 | try 116 | { 117 | if (string.IsNullOrWhiteSpace(userName)) 118 | userName = App.Console.Read("UserName: "); 119 | 120 | ValidateInfo(userName, nameof(userName)); 121 | 122 | var currentUser = this.GetUser(userName); 123 | if (currentUser == null || !currentUser.IsValid()) 124 | { 125 | currentUser = new NuUser(userName, password); 126 | var httpClient = this.CreateNuHttpClient(currentUser, nameof(Login)); 127 | var endPointRepository = new EndPointApi(httpClient); 128 | var authRepository = new NuAuthApi(httpClient, endPointRepository); 129 | 130 | if (string.IsNullOrWhiteSpace(password)) 131 | password = App.Console.Read("Password: "); 132 | ValidateInfo(password, nameof(password)); 133 | 134 | // Inicia o login 135 | StartLogin(authRepository); 136 | 137 | currentUser.CleanPassword(); 138 | this.SaveUser(currentUser); 139 | } 140 | 141 | this.SetCurrentUser(userName); 142 | 143 | App.Console.Success($"Sucesso! Login efetuado com '{currentUser.GetLoginType()}'"); 144 | App.Console.Write($" "); 145 | App.Console.Write($"LOCALIZAÇÃO USUÁRIO:"); 146 | App.Console.Write($" {this.GetUserFileName(currentUser)}"); 147 | App.Console.Write($" "); 148 | App.Console.Warning($"Seu token expira em: {currentUser.GetExpiredDate()}"); 149 | } 150 | catch (Exception ex) 151 | { 152 | this.ShowApiException(ex); 153 | } 154 | } 155 | 156 | private void StartLogin(NuAuthApi authRepository) 157 | { 158 | authRepository.Login(out var needLoginValidation); 159 | 160 | if (needLoginValidation) 161 | { 162 | var code = Guid.NewGuid().ToString(); 163 | 164 | if (!this.AppSettings.EnableMockServer) 165 | { 166 | var qrCodeData = new QRCodeGenerator().CreateQrCode(code, QRCodeGenerator.ECCLevel.Q); 167 | var qrCode = new AsciiQRCode(qrCodeData).GetGraphic(1); 168 | 169 | App.Console.Warning("Você deve se autenticar com seu telefone para poder acessar seus dados."); 170 | App.Console.Warning("Digitalize o QRCode abaixo com seu aplicativo Nubank no seguinte menu:"); 171 | App.Console.Warning("Ícone de configurações > Perfil > Acesso pelo site"); 172 | 173 | App.Console.Write(" "); 174 | App.Console.Write(qrCode); 175 | 176 | App.Console.Write(" "); 177 | App.Console.Warning($"Use seu telefone para escanear e depois disso pressione a tecla 'enter' para continuar ..."); 178 | 179 | App.Console.Read(); 180 | } 181 | 182 | authRepository.ValidateQRCode(code); 183 | } 184 | } 185 | 186 | private void ValidateInfo(string info, string infoName) 187 | { 188 | if (string.IsNullOrWhiteSpace(info)) 189 | throw new Exception($"Nenhum '{infoName}' informado"); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NubankCli 2 | 3 | NubankCli é um aplicativo de console que importa as transações do cartão de crédito e da NuConta em forma de arquivos JSONs segregados por usuário. Além disso, ele prove alguns comandos simples que ajudam a visualizar/sumarizar as transações importadas via linha de comando. 4 | 5 | # Inicio rápido 6 | 7 | Para iniciar é muito rápido e você pode fazer isso em qualquer terminal. Todo o material é focado no bash, caso queria usar outro terminal será necessário adaptação. 8 | 9 | Um ponto importante é que a execução dessa CLI foi feita para rodar ao lado do código fonte, como acontece com outras linguagens como Python, Node e PHP. Isso ajuda em evoluções e correções de bugs, um `.exe` compilado seria uma caixa preta e não é o meu proposito. 10 | 11 | Vale destacar que caso queira faze-lo nada deverá te impedir, basta executar o comando `dotnet publish -c Release` e obter o compilado. 12 | 13 | 1. Caso não tenha o `dotnet core` instalado, faça-o pelo link: https://dotnet.microsoft.com/download 14 | 2. Faça o clone do projeto no seu local preferido e defina o comando simplificador `nu`: 15 | 16 | ```bash 17 | # Baixa o código na sua pasta corrente 18 | git clone https://github.com/juniorgasparotto/NubankCli.git 19 | 20 | # Entra na pasta 21 | cd NubankCli 22 | 23 | # Faça o build do código para evitar que a compilação seja feita no primeiro uso 24 | dotnet build "src/NubankCli/NubankCli.csproj" 25 | 26 | # Define um alias para o arquivo ./nu no qual contém um simplificador da execução do .net 27 | alias nu=./nu 28 | 29 | # Ou defina de maneira global e permanente 30 | echo alias nu="`pwd`/nu" >> ~/.bashrc; source ~/.bashrc; 31 | ``` 32 | 33 | 3. Faça o login na CLI usando suas credencias do aplicativo NuBank 34 | 35 | ``` 36 | nu login [cpf] [senha] 37 | ``` 38 | 39 | * As informações do seu login ficarão salvas na pasta `src/NubankCli/UsersData/[cpf]`. Aqui temos o arquivo `user-info` que contém o seu token atual e os links descobertos da sua conta. 40 | 41 | * Você permanecerá logado até que o token do NuBank expire, normalmente demora-se 7 dias. 42 | 43 | 4. Após isso, será necessário fazer a segunda parte da autenticação e se tudo ocorreu bem, você verá um QRCode no seu terminal. Utilize o seu aplicativo do nubank em seu celular para validar o QRCode, use o menu: 44 | 45 | ``` 46 | Ícone de configurações > Perfil > Acesso pelo site 47 | ``` 48 | 49 | 5. Após a validação pelo celular, digite `enter` para prosseguir 50 | 51 | 6. Se tudo ocorrer bem você já estará logado e agora será possível importar suas transações de crédito e débito 52 | 53 | 7. Para importar as transações do cartão de crédito use: 54 | 55 | ```bash 56 | # Importa tudo sem nenhum filtro 57 | nu import-credit 58 | 59 | # Importa com filtro de data de inicio apenas 60 | nu import-credit --start 2020-01-01 61 | 62 | # Importa com filtro de data fim apenas 63 | nu import-credit --end 2020-02-01 64 | 65 | # Importa com filtro de data de inicio e fim 66 | nu import-credit --start 2020-01-01 --end 2020-02-01 67 | ``` 68 | 69 | 8. Para importar as transações do cartão de débito (nuconta) use: 70 | 71 | ```bash 72 | # Importa tudo sem nenhum filtro 73 | nu import-debit 74 | 75 | # Importa com filtro de data de inicio apenas 76 | nu import-debit --start 2020-01-01 77 | 78 | # Importa com filtro de data fim apenas 79 | nu import-debit --end 2020-02-01 80 | 81 | # Importa com filtro de data de inicio e fim 82 | nu import-debit --start 2020-01-01 --end 2020-02-01 83 | ``` 84 | 85 | 9. Para visualizar os extratos do seu cartão de crédito que foram importados, utilize o comando: 86 | 87 | ```bash 88 | # Visualiza todos os extratos importados de acordo com as datas de abertura e fechamento cada boleto 89 | # Pode usar nas 3 formas: Simplificada, Singular e Plural 90 | nu get stat creditcard 91 | nu get statement creditcard 92 | nu get statements creditcard 93 | 94 | # Visualiza todos os extratos importados de forma mensal (forma longa) 95 | nu get stat creditcard --by-month 96 | ``` 97 | 98 | * Os dados importados de cartão de crédito ficaram dentro da sua pasta de usuário nas sub-pastas: `src/NubankCli/UsersData/[cpf]/card-credit` 99 | 100 | 10. Para visualizar os extratos do seu cartão de débito que foram importados, utilize o comando: 101 | 102 | ```bash 103 | # Pode usar nas 3 formas: Simplificada, Singular e Plural 104 | nu get stat nuconta 105 | nu get statement nuconta 106 | nu get statements nuconta 107 | ``` 108 | 109 | * Os dados importados de cartão de débito ficaram dentro da sua pasta de usuário nas sub-pastas: `src/NubankCli/UsersData/[cpf]/nuconta`. 110 | 111 | 11. Para visualizar os extratos consolidados do cartão de crédito e débito (nuconta), utilize o comando: 112 | 113 | ```bash 114 | # Exibe extratos do cartão de crédito e débito (Pode usar nas 3 formas: Simplificada, Singular e Plural) 115 | nu get stat 116 | nu get statement 117 | nu get statements 118 | 119 | # Exibe extratos consolidando ambos os cartões de forma mensal (forma longa) 120 | nu get stat --merge 121 | 122 | # Exibe extratos consolidando ambos os cartões de forma mensal (forma curta) 123 | nu get stat -m 124 | ``` 125 | 126 | 12. Para visualizar todas as transações importadas, utilize o comando: 127 | 128 | ```bash 129 | # Simplificada 130 | nu get trans 131 | 132 | # Singular 133 | nu get transaction 134 | 135 | # Plural 136 | nu get transactions 137 | 138 | # Para visualizar mais colunas 139 | nu get transactions -o wide 140 | 141 | # Visualizar transações que iniciam com "IOF": 142 | nu get transactions "IOF" 143 | 144 | # Visualizar transações que tenham o ID: 145 | nu get transactions "5f52f4f6" 146 | ``` 147 | 148 | 13. Para verificar quem está logado + informações do usuário: 149 | 150 | ```bash 151 | nu whoami 152 | ``` 153 | 154 | 14. Para deslogar utilize o comando abaixo ou apague o arquivo `src/NubankCli/UsersData/[cpf]/user-info.json`: 155 | 156 | ```bash 157 | nu logout 158 | ``` 159 | 160 | # Outros comandos 161 | 162 | Para importar os dados de cartão de crédito de forma mensal: 163 | 164 | ```bash 165 | nu import-credit --statement-type ByMonth 166 | ``` 167 | 168 | Obtém apenas os extratos no qual contém alguma entrada SEM considerar pagamentos de boletos: 169 | 170 | ```bash 171 | nu get stat --where 'Transactions.Where(t => t.Value > 0 && !t.IsBillPayment).Sum(t => t.Value) > 0' 172 | ``` 173 | 174 | Ordena os extratos por valor de entrada do maior para o menor: 175 | 176 | ```bash 177 | nu get stat --sort 'Transactions.Where(t => t.Value > 0).Sum(t => t.Value) DESC' 178 | ``` 179 | 180 | Comandos avançados para filtragem das transações importadas: 181 | 182 | ```bash 183 | # Obtem as transações com filtro (forma curta -w): Apenas transações de entrada de valor (recebimentos) 184 | nu get trans -w "Value > 0" 185 | 186 | # Obtem as transações com filtro (forma longa --where): Apenas transações de saída de valor (pagamentos) 187 | nu get trans --where "Value < 0" 188 | 189 | # Obtem as transações ordenas por data (forma curta -s): Menor para o maior (mais antigas primeiro) 190 | nu get trans -s "EventDate ASC" 191 | 192 | # Obtem as transações ordenas por data (forma longa --sort): Maior para o menor (mais recentes primeiro) 193 | nu get trans --sort "EventDate DESC" 194 | 195 | # Obtem as maiores transações de saída 196 | nu get trans -w "Value < 0" --sort "Value ASC" 197 | 198 | # Obtem as maiores transações de entrada 199 | nu get trans -w "Value > 0" --sort "Value DESC" 200 | 201 | # Filtra pelo nome do cartão (nesse caso não obtem nada da NuConta) 202 | nu get trans -w 'CardName="credit-card"' 203 | 204 | # Obtem as transações de um extrato especifico (Start é a data de abertura da fatura - OpenDate) 205 | nu get trans -w 'Statement.Start == "2020-05-17" && !IsBillPayment' -s "PostDate DESC, EventDate DESC" 206 | 207 | # Obtem com uma quantidade mair de itens por página 208 | nu get trans -S 100 209 | 210 | # Obtem sem paginação 211 | nu get trans -S 0 212 | 213 | # Obtem em formato de JSON sem paginação removendo o cabeçalho e rodapé 214 | nu get trans -o json -S 0 --verbose none 215 | ``` 216 | 217 | # Contribuíndo 218 | 219 | Para contribuir basta ter instalado o `Visual Studio Code` ou o próprio `Visual Studio Community` e fazer as adaptações que ache necessárias. Vale dizer que o projeto é bem simples e não contém diversos recursos como: 220 | 221 | * Geração de boleto 222 | * Conta de investimento 223 | * Login com certificado 224 | * Refresh Token 225 | 226 | Fique a vontade para fazer essas evoluções ;) 227 | 228 | ## Wiremock 229 | 230 | A pasta `Wiremock` contém algumas massas de dados que pode auxiliar na correção de bugs ou evoluções. Para usa-lo, basta instalar o wiremock e usar os arquivos desta pasta na execução do .jar do wiremock. 231 | 232 | Os passos para executar usando o wiremock são: 233 | 234 | 1. Baixe o wiremock standalone: http://wiremock.org/docs/running-standalone/ 235 | 2. Execute o wiremock apontando para a pasta onde temos a nossa massa de dados de testes: 236 | 237 | ```bash 238 | java -jar "C:\wiremock-standalone-2.27.2.jar" --port 6511 --root-dir "C:\NubankCli\wiremock" --match-headers Content-Type,Authorization,Accept 239 | ``` 240 | 241 | * OBS: Estou considerando que todos os artefatos estejam na `C:`, troque para o caminho onde você baixou o `Wiremock` e o `NubankCli`. 242 | 243 | 3. Abra o arquivo `C:\NubankCli\src\NubankCli\settings.json` e altere a propriedade: `enableMockServer: true` 244 | 4. Por padrão, a porta `6511` já está configurada nesse arquivo na propriedade `mockUrl`, caso queira altera-la, mude o arquivo de configurações e execute o wiremock novamente na porta correta. 245 | 246 | ## Wiremock UI 247 | 248 | Caso queria usar uma interface para o `Wiremock`, eu aconselho o `WiremockUI`, uma interface criada por mim que pode te ajudar a visualizar e manipular os servidores e arquivos do wiremock. Vale dizer que é uma interface exclusiva para Windows, para outros S.O é necessário usar o `.jar` diretamente. 249 | 250 | https://github.com/juniorgasparotto/WiremockUI 251 | 252 | ## SysCommand 253 | 254 | Esse projeto usa a biblioteca `SysCommand`, um parser de linha de comando para `.net` criado por mim que simplifica todo o trabalho de aplicações para console. O link abaixo contém todas as informações: 255 | 256 | https://github.com/juniorgasparotto/SysCommand 257 | 258 | 259 | ## Agradecimentos 260 | 261 | Esse projeto foi desenvolvido baseado na ideia de alguns outros repositórios no qual gostaria de fazer os devidos agradecimentos: 262 | 263 | * https://github.com/lira92/nubank-dotnet 264 | * https://github.com/andreroggeri/pynubank 265 | * https://github.com/SpentBook/nubank-importer 266 | -------------------------------------------------------------------------------- /src/NubankSharp/Repositories/Api/NuHttpClient.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using NubankSharp.Entities; 3 | using NubankSharp.Extensions; 4 | using RestSharp; 5 | using RestSharp.Serializers.Newtonsoft.Json; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Security.Cryptography.X509Certificates; 11 | using System.Text; 12 | using System.Web; 13 | using RestRequest = RestSharp.Serializers.Newtonsoft.Json.RestRequest; 14 | 15 | namespace NubankSharp.Repositories.Api 16 | { 17 | public class NuHttpClient 18 | { 19 | public const string USER_AGENT = "NubankCli"; 20 | 21 | private readonly RestClient _client = new(); 22 | private readonly NuHttpClientLogging _logging; 23 | 24 | public string NubankUrl { get; } 25 | public string NubankUrlMock { get; } 26 | public NuUser User { get; } 27 | 28 | public NuHttpClient(NuUser user, string nubankUrl, string nubankUrlMock = null, NuHttpClientLogging logging = null) 29 | { 30 | this.User = user; 31 | this.NubankUrl = nubankUrl; 32 | this.NubankUrlMock = nubankUrlMock; 33 | this._logging = logging; 34 | 35 | _client.AddHandler("application/json", new NewtonsoftJsonSerializer()); 36 | _client.AddDefaultHeader("Content-Type", "application/json"); 37 | _client.AddDefaultHeader("X-Correlation-Id", "WEB-APP.pewW9"); 38 | _client.AddDefaultHeader("User-Agent", USER_AGENT); 39 | } 40 | 41 | private void AuthenticateIfLogged() 42 | { 43 | if (this.User.CertificateBase64 != null && (_client.ClientCertificates == null || _client.ClientCertificates.Count == 0)) 44 | { 45 | var bytes = Convert.FromBase64String(this.User.CertificateBase64); 46 | SetCertificate(new X509Certificate2(bytes)); 47 | } 48 | 49 | if (this.User.Token != null) 50 | { 51 | var authHeader = _client.DefaultParameters.FirstOrDefault(f => f.Type == ParameterType.HttpHeader && f.Name == "Authorization"); 52 | if (authHeader == null) 53 | SetToken(this.User.Token); 54 | } 55 | } 56 | 57 | public void SetCertificate(X509Certificate2 certificate2) 58 | { 59 | if (certificate2 != null) 60 | { 61 | _client.ClientCertificates ??= new X509CertificateCollection(); 62 | _client.ClientCertificates.Add(certificate2); 63 | } 64 | } 65 | 66 | public void SetToken(string token) 67 | { 68 | if (token != null) 69 | { 70 | _client.AddDefaultHeader("Authorization", $"Bearer {this.User.Token}"); 71 | } 72 | } 73 | 74 | public T Get(string name, string url, out IRestResponse response, params int[] allowedStatusCode) 75 | { 76 | this.AuthenticateIfLogged(); 77 | 78 | url = GetUrl(url); 79 | _client.BaseUrl = new Uri(url); 80 | 81 | response = _client.Get(new RestRequest()); 82 | return GetResponseOrException(name, response, "GET", url, allowedStatusCode); 83 | } 84 | 85 | public T Get(string name, string url, Dictionary headers, out IRestResponse response, params int[] allowedStatusCode) 86 | { 87 | this.AuthenticateIfLogged(); 88 | 89 | var request = new RestRequest(); 90 | url = GetUrl(url); 91 | _client.BaseUrl = new Uri(url); 92 | 93 | if (headers != null) 94 | { 95 | headers.ToList().ForEach((KeyValuePair header) => 96 | { 97 | request.AddHeader(header.Key, header.Value); 98 | }); 99 | } 100 | 101 | response = _client.Get(request); 102 | return GetResponseOrException(name, response, "GET", url, allowedStatusCode); 103 | } 104 | 105 | public T Post(string name, string url, object body, out IRestResponse response, params int[] allowedStatusCode) 106 | { 107 | this.AuthenticateIfLogged(); 108 | 109 | var request = new RestRequest(); 110 | url = GetUrl(url); 111 | _client.BaseUrl = new Uri(url); 112 | 113 | request.AddJsonBody(body); 114 | 115 | response = _client.Post(request); 116 | 117 | return GetResponseOrException(name, response, "POST", url, allowedStatusCode); 118 | } 119 | 120 | public string Post(string name, string url, object body, Dictionary headers, out IRestResponse response, params int[] allowedStatusCode) 121 | { 122 | this.AuthenticateIfLogged(); 123 | 124 | var request = new RestRequest(); 125 | url = GetUrl(url); 126 | _client.BaseUrl = new Uri(url); 127 | 128 | if (headers != null) 129 | { 130 | headers.ToList().ForEach((KeyValuePair header) => 131 | { 132 | request.AddHeader(header.Key, header.Value); 133 | }); 134 | } 135 | 136 | request.AddJsonBody(body); 137 | response = _client.Post(request); 138 | return GetResponseOrException(name, response, "POST", url, allowedStatusCode); 139 | } 140 | 141 | public T Post(string name, string url, object body, Dictionary headers, out IRestResponse response, params int[] allowedStatusCode) 142 | { 143 | this.AuthenticateIfLogged(); 144 | 145 | var request = new RestRequest(); 146 | url = GetUrl(url); 147 | _client.BaseUrl = new Uri(url); 148 | 149 | if (headers != null) 150 | { 151 | headers.ToList().ForEach((KeyValuePair header) => 152 | { 153 | request.AddHeader(header.Key, header.Value); 154 | }); 155 | } 156 | 157 | request.AddJsonBody(body); 158 | response = _client.Post(request); 159 | return GetResponseOrException(name, response, "POST", url, allowedStatusCode); 160 | } 161 | 162 | private string GetUrl(string url) 163 | { 164 | if (!string.IsNullOrWhiteSpace(this.NubankUrlMock)) 165 | { 166 | var mockUrl = new Uri(this.NubankUrlMock); 167 | var builder = new UriBuilder(url) 168 | { 169 | Scheme = mockUrl.Scheme, 170 | Host = mockUrl.Host, 171 | Port = mockUrl.Port, 172 | }; 173 | 174 | url = builder.Uri.ToString(); 175 | 176 | // TODO: uma forma melhor seria usar URI para fazer a subustituição do host 177 | //url = url.Replace(this.NubankUrl, this.NubankUrlMock); 178 | 179 | //url = url.Replace("https://prod-s0-webapp-proxy.nubank.com.br", this.NubankUrlMock); 180 | //url = url.Replace("https://prod-global-webapp-proxy.nubank.com.br", this.NubankUrlMock); 181 | } 182 | 183 | return url; 184 | } 185 | 186 | private T GetResponseOrException(string name, IRestResponse response, string verb, string url, params int[] allowedStatusCode) 187 | { 188 | SaveContentToFile(url, response, name); 189 | CheckStatus(response, verb, url, allowedStatusCode); 190 | return response.Data; 191 | } 192 | 193 | private string GetResponseOrException(string name, IRestResponse response, string verb, string url, params int[] allowedStatusCode) 194 | { 195 | SaveContentToFile(url, response, name); 196 | CheckStatus(response, verb, url, allowedStatusCode); 197 | return response.Content; 198 | } 199 | 200 | private void CheckStatus(IRestResponse response, string verb, string url, int[] allowedStatusCode) 201 | { 202 | var statusCode = (int)response.StatusCode; 203 | if ((statusCode == 0 || statusCode > 299) && !allowedStatusCode.Contains(statusCode)) 204 | throw response.ErrorException ?? new Exception($"{verb} {url} - ({statusCode}) {response.ErrorMessage ?? response.StatusDescription}"); 205 | } 206 | 207 | /// 208 | /// Save content in file 209 | /// 210 | /// Content to save 211 | /// File location 212 | public void SaveContentToFile(string url, IRestResponse response, string name) 213 | { 214 | if (_logging != null && this._logging != null) 215 | { 216 | var stringBuilder = new StringBuilder(); 217 | stringBuilder.AppendLine($"-> REQUEST"); 218 | stringBuilder.AppendLine(); 219 | stringBuilder.AppendLine($"{response.Request.Method} {url}"); 220 | var hasJson = false; 221 | var hasBody = false; 222 | 223 | foreach (var h in response.Request.Parameters) 224 | { 225 | if (h.Type != ParameterType.RequestBody) 226 | { 227 | stringBuilder.AppendLine($"{h.Name}: {h.Value}"); 228 | if (h.Value.ToString().Contains("json")) 229 | hasJson = true; 230 | } 231 | else 232 | { 233 | hasBody = true; 234 | } 235 | } 236 | 237 | if (hasBody && hasJson) 238 | stringBuilder.AppendLine(StringExtensions.BeautifyJson(response.Request.Body.Value.ToString())); 239 | else if (hasBody) 240 | stringBuilder.AppendLine(response.Request.Body.Value.ToString()); 241 | 242 | stringBuilder.AppendLine(); 243 | 244 | stringBuilder.AppendLine($"-> RESPONSE"); 245 | stringBuilder.AppendLine(); 246 | stringBuilder.AppendLine($"StatusCode: {(int)response.StatusCode} ({response.StatusCode})"); 247 | foreach (var h in response.Headers) 248 | { 249 | stringBuilder.AppendLine($"{h.Name}: {h.Value}"); 250 | } 251 | 252 | var fileName = $"{_logging.Folder}/{_logging.UserName}/{_logging.Scope}/Nubank-{name}"; 253 | 254 | var ext = hasJson ? ".json" : null; 255 | fileName = $"{fileName}{ext}"; 256 | 257 | if (!string.IsNullOrWhiteSpace(response.Content)) 258 | { 259 | stringBuilder.AppendLine(); 260 | stringBuilder.AppendLine(hasJson ? StringExtensions.BeautifyJson(response.Content) : response.Content); 261 | } 262 | 263 | var count = 0; 264 | while (File.Exists(fileName)) 265 | { 266 | count++; 267 | fileName = Path.GetDirectoryName(fileName) 268 | + Path.DirectorySeparatorChar 269 | + Path.GetFileNameWithoutExtension(fileName) 270 | + count.ToString() 271 | + Path.GetExtension(fileName); 272 | } 273 | 274 | FileExtensions.CreateFolderIfNeeded(fileName); 275 | File.WriteAllText(fileName, stringBuilder.ToString()); 276 | } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/NubankCli/Extensions/CommandExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NubankSharp.Extensions 2 | { 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Newtonsoft.Json; 5 | using NubankSharp.Extensions.Tables; 6 | using NubankSharp.Extensions.Langs; 7 | using NubankSharp.Entities; 8 | using NubankSharp.Exceptions; 9 | using NubankSharp.Repositories.Api; 10 | using SysCommand.ConsoleApp; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.IO; 14 | using System.Linq; 15 | using System.Linq.Dynamic.Core; 16 | using System.Runtime.InteropServices; 17 | using System.Security; 18 | using Microsoft.Extensions.Options; 19 | using NubankSharp.Cli.Extensions; 20 | using NubankSharp.Repositories.Files; 21 | using NubankSharp.Models; 22 | 23 | public static class CommandExtensions 24 | { 25 | public static Guid _userId; 26 | 27 | private const string LINE_SEPARATOR = "\r\n\r\n"; 28 | public static readonly string QUERY_FOLDER = Path.Combine(EnvironmentExtensions.ProjectRootOrExecutionDirectory, "Queries"); 29 | public static readonly string USERSDATA_FOLDER = Path.Combine(EnvironmentExtensions.ProjectRootOrExecutionDirectory, "UsersData"); 30 | public static readonly string SESSION_FILE_PATH = Path.Combine(USERSDATA_FOLDER, "session.json"); 31 | public static readonly string USER_FILE_NAME = "nu-user.json"; 32 | public static NuSession SESSION = null; 33 | public const int FIRST_PAGE = 1; 34 | 35 | #region Session 36 | 37 | public static NuSession GetSession(this Command command) 38 | { 39 | if (SESSION == null) 40 | { 41 | var repo = command.GetService>(); 42 | var nuSession = repo.GetFile(SESSION_FILE_PATH); 43 | SESSION = nuSession ?? new NuSession(); 44 | } 45 | return SESSION; 46 | } 47 | 48 | public static void SaveSession(this Command command) 49 | { 50 | var nuSession = GetSession(command); 51 | var repo = command.GetService>(); 52 | repo.Save(nuSession, SESSION_FILE_PATH); 53 | } 54 | 55 | public static NuUser GetCurrentUser(this Command command) 56 | { 57 | var session = GetSession(command); 58 | if (string.IsNullOrWhiteSpace(session?.CurrentUser)) 59 | throw new UnauthorizedException(); 60 | 61 | var repo = command.GetService>(); 62 | return repo.GetFile(command.GetUserFileName(session.CurrentUser)); 63 | } 64 | 65 | public static void SetCurrentUser(this Command command, string userName) 66 | { 67 | var session = GetSession(command); 68 | session.CurrentUser = userName; 69 | SaveSession(command); 70 | } 71 | 72 | #endregion 73 | 74 | #region UsersData 75 | 76 | public static string GetUserPath(this Command command, NuUser user) 77 | { 78 | return command.GetUserPath(user.UserName); 79 | } 80 | 81 | public static string GetUserPath(this Command command, string userName) 82 | { 83 | return Path.Combine(EnvironmentExtensions.ProjectRootOrExecutionDirectory, "UsersData", userName); 84 | } 85 | 86 | public static string GetUserFileName(this Command command, NuUser user) 87 | { 88 | return command.GetUserFileName(user.UserName); 89 | } 90 | 91 | public static string GetUserFileName(this Command command, string userName) 92 | { 93 | return Path.Combine(command.GetUserPath(userName), USER_FILE_NAME); 94 | } 95 | 96 | public static string GetCardPath(this Command command, Card card) 97 | { 98 | return Path.Combine(command.GetUserPath(card.UserName), card.Name); 99 | } 100 | 101 | public static string GetStatementFileName(this Command command, Statement statement) 102 | { 103 | var dtStart = statement.Start.ToString("yyyy-MM"); 104 | var dtEnd = statement.End.ToString("yyyy-MM"); 105 | var fileName = dtStart; 106 | 107 | if (dtStart != dtEnd) 108 | fileName += $"_{dtEnd}"; 109 | 110 | return Path.Combine(command.GetCardPath(statement.Card), $"{fileName}.json"); 111 | } 112 | 113 | public static NuUser GetUser(this Command command, string userName) 114 | { 115 | var repo = command.GetService>(); 116 | return repo.GetFile(command.GetUserFileName(userName)); 117 | } 118 | 119 | public static void SaveUser(this Command command, NuUser user) 120 | { 121 | var repo = command.GetService>(); 122 | repo.Save(user, command.GetUserFileName(user.UserName)); 123 | } 124 | 125 | #endregion 126 | 127 | public static NuHttpClient CreateNuHttpClient(this Command command, NuUser user, string scope = null) 128 | { 129 | var appSettings = command.GetService>().Value; 130 | return new NuHttpClient( 131 | user, 132 | appSettings.NubankUrl, 133 | appSettings.EnableMockServer ? appSettings.MockUrl : null, 134 | appSettings.EnableDebugFile ? new NuHttpClientLogging(user.UserName, scope, Path.Combine(EnvironmentExtensions.ProjectRootOrExecutionDirectory, "Logs")) : null 135 | ); 136 | } 137 | 138 | public static NuApi CreateNuApiByUser(this Command command, NuUser user, string scope = null) 139 | { 140 | if (user.Token == null) 141 | throw new UnauthorizedException(); 142 | 143 | var httpClient = command.CreateNuHttpClient(user, scope); 144 | var endPointRepository = new EndPointApi(httpClient); 145 | var repository = new NuApi(httpClient, endPointRepository, new GqlQueryRepository(QUERY_FOLDER)); 146 | return repository; 147 | } 148 | 149 | public static T GetService(this Command command) 150 | { 151 | return command.App.Items.GetServiceProvider().GetService(); 152 | } 153 | 154 | public static object GetService(this Command command, Type type) 155 | { 156 | return command.App.Items.GetServiceProvider().GetService(type); 157 | } 158 | 159 | public static void ShowApiException(this Command command, Exception exception) 160 | { 161 | while (exception is AggregateException) 162 | exception = exception.InnerException; 163 | 164 | if (check(exception, out var unauthorizedException)) 165 | { 166 | command.App.Console.Error("Você não está logado"); 167 | } 168 | else 169 | { 170 | command.App.Console.Error(exception.Message); 171 | command.App.Console.Error(exception.StackTrace); 172 | } 173 | 174 | static bool check(Exception eIn, out TVerify eOut) where TVerify : Exception 175 | { 176 | eOut = default(TVerify); 177 | if (eIn is TVerify verify) 178 | { 179 | eOut = verify; 180 | return true; 181 | } 182 | else if (eIn.InnerException is TVerify verify1) 183 | { 184 | eOut = verify1; 185 | return true; 186 | } 187 | 188 | return false; 189 | } 190 | } 191 | public static void ViewPagination(this Command command, int page, Func> callback, bool autoPage, string output = null, bool showCountResume = true) 192 | { 193 | var next = true; 194 | while (next) 195 | { 196 | var paged = callback(page); 197 | string text; 198 | 199 | if (paged.PageCount == 0) 200 | text = MessagesPtBr.SEARCH_EMPTY_RESULT; 201 | else if (output == "json") 202 | text = JsonConvert.SerializeObject(paged.Queryable, Formatting.Indented); 203 | else 204 | text = ToTable(command, paged.Queryable, output); 205 | 206 | command.App.Console.Write(text); 207 | 208 | if (paged.PageCount == 1 && showCountResume) 209 | command.App.Console.Success(AddLineSeparator(string.Format(MessagesPtBr.SEARCH_RESULT, paged.RowCount))); 210 | else if (paged.PageCount > 1) 211 | command.App.Console.Success(AddLineSeparator(string.Format(MessagesPtBr.SEARCH_PAGINATION_RESULT, paged.CurrentPage, paged.PageCount, paged.RowCount))); 212 | 213 | if (!autoPage) 214 | break; 215 | 216 | next = page < paged.PageCount; 217 | page++; 218 | 219 | if (next && command.App.Console.Read("") != null) 220 | break; 221 | } 222 | } 223 | 224 | public static void ViewFormatted(this Command command, IEnumerable value, string output = null, bool showCountResume = true) 225 | { 226 | string text; 227 | bool hasItems = value.Any(); 228 | 229 | if (!hasItems) 230 | text = MessagesPtBr.SEARCH_EMPTY_RESULT; 231 | else if (output == "json") 232 | text = Newtonsoft.Json.JsonConvert.SerializeObject(value, Formatting.Indented); 233 | else 234 | text = ToTable(command, value, output); 235 | 236 | command.App.Console.Write(text); 237 | 238 | if (hasItems && showCountResume) 239 | command.App.Console.Success(AddLineSeparator(string.Format(MessagesPtBr.SEARCH_RESULT, value.Count()))); 240 | } 241 | 242 | public static void ViewSingleFormatted(this Command command, T value, string output) 243 | { 244 | string text; 245 | if (output == "json") 246 | text = JsonConvert.SerializeObject(value, Formatting.Indented); 247 | else 248 | text = ToTable(command, new List() { value }, output); 249 | 250 | command.App.Console.Write(text); 251 | } 252 | 253 | public static SecureString ReadPassword(this Command command, string msg) 254 | { 255 | var pass = new SecureString(); 256 | // var colorOriginal = Console.ForegroundColor; 257 | // Console.ForegroundColor = ConsoleColor.Blue; 258 | 259 | if (command.App.Console.BreakLineInNextWrite) 260 | Console.WriteLine(); 261 | 262 | Console.Write(msg); 263 | // Console.ForegroundColor = colorOriginal; 264 | ConsoleKeyInfo key; 265 | 266 | do 267 | { 268 | key = Console.ReadKey(true); 269 | 270 | // Backspace Should Not Work 271 | if (!char.IsControl(key.KeyChar)) 272 | { 273 | pass.AppendChar(key.KeyChar); 274 | Console.Write("*"); 275 | } 276 | else 277 | { 278 | if (key.Key == ConsoleKey.Backspace && pass.Length > 0) 279 | { 280 | pass.RemoveAt(pass.Length - 1); 281 | Console.Write("\b \b"); 282 | } 283 | } 284 | } 285 | // Stops receiving keys once enter is pressed 286 | while (key.Key != ConsoleKey.Enter); 287 | 288 | Console.WriteLine(); 289 | command.App.Console.BreakLineInNextWrite = false; 290 | return pass; 291 | } 292 | 293 | public static string SecureStringToString(this Command command, SecureString value) 294 | { 295 | IntPtr valuePtr = IntPtr.Zero; 296 | try 297 | { 298 | valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value); 299 | return Marshal.PtrToStringUni(valuePtr); 300 | } 301 | finally 302 | { 303 | Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 304 | } 305 | } 306 | 307 | public static T Get(this IEnumerable collection) where T : Command 308 | { 309 | return collection.Where(f => f is T).Cast().FirstOrDefault(); 310 | } 311 | 312 | public static bool Continue(this Command command, string message, params string[] yesOptions) 313 | { 314 | return Continue(command, message, out _, yesOptions); 315 | } 316 | 317 | public static bool Continue(this Command command, string message, out string result, params string[] yesOptions) 318 | { 319 | result = command.App.Console.Read(message + " [Yes/No]: ")?.ToLower(); 320 | 321 | if (yesOptions.Length == 0) 322 | yesOptions = new string[] { "y", "yes", "s", "sim" }; 323 | 324 | return !string.IsNullOrWhiteSpace(result) && yesOptions.Contains(result); 325 | } 326 | 327 | private static string ToTable(Command command, IEnumerable value, string output) 328 | { 329 | TableView tableView; 330 | var tableConfig = command.App.Items.GetServiceProvider().GetService>(); 331 | if (tableConfig == null) 332 | tableView = TableView.ToTableView(value, wide: output == "wide"); 333 | else 334 | tableView = TableView.ToTableView(value, tableConfig, output == "wide"); 335 | 336 | tableView.AddLineSeparator = false; 337 | tableView.ColumnSeparator = null; 338 | 339 | return tableView.Build().ToString(); 340 | } 341 | 342 | public static string GetTempFile(string prefix, string extension) 343 | { 344 | if (!extension.StartsWith(".")) 345 | extension = "." + extension; 346 | 347 | if (string.IsNullOrWhiteSpace(prefix)) 348 | prefix = typeof(T).Name; 349 | 350 | string fileName; 351 | int count = 0; 352 | 353 | while (true) 354 | { 355 | fileName = $"{prefix}{count}{extension}"; 356 | fileName = Path.Combine(Path.GetTempPath(), fileName); 357 | 358 | if (!File.Exists(fileName)) 359 | { 360 | using var file = new FileStream(fileName, FileMode.CreateNew); 361 | break; 362 | } 363 | 364 | count++; 365 | } 366 | 367 | return fileName; 368 | } 369 | 370 | #region Privates 371 | 372 | private static string AddLineSeparator(string msg) 373 | { 374 | return string.Format("{0}{1}", LINE_SEPARATOR, msg); 375 | } 376 | 377 | #endregion 378 | 379 | } 380 | } --------------------------------------------------------------------------------