Exemplo em C#

Este documento contém um exemplo completo de implementação da API Flowbiz em C# utilizando ASP.NET Core.

Código Completo

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace FlowbizApi
{
    // Modelos de dados
    public class Brand
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public class ProductCategory
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public class Variant
    {
        public string Sku { get; set; }
        public string Name { get; set; }
        public double Price { get; set; }
        public double? PriceFrom { get; set; }
        public int? Stock { get; set; }
        public string ImageUrl { get; set; }
        public string Url { get; set; }
        public bool? Available { get; set; }
    }

    public class Product
    {
        public string ProductId { get; set; }
        public string Url { get; set; }
        public Brand Brand { get; set; }
        public List<ProductCategory> Category { get; set; }
        public List<Variant> Variants { get; set; }
    }

    public class OrderItem
    {
        public string ProductId { get; set; }
        public List<ProductCategory> Categories { get; set; }
        public string Sku { get; set; }
        public string Name { get; set; }
        public string Brand { get; set; }
        public double Price { get; set; }
        public int Quantity { get; set; }
        public string Url { get; set; }
        public string ImageUrl { get; set; }
    }

    public class PaymentMethod
    {
        public string Type { get; set; }
        public double Amount { get; set; }
    }

    public class DeliveryMethod
    {
        public string Type { get; set; }
        public double Amount { get; set; }
    }

    public class DeliveryAddress
    {
        public string City { get; set; }
        public string AddressLine2 { get; set; }
        public string Neighborhood { get; set; }
        public string AddressNumber { get; set; }
        public string State { get; set; }
        public string AddressLine1 { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }

    public class Order
    {
        public string Platform { get; set; }
        public string OrderId { get; set; }
        public DateTime Date { get; set; }
        public double Subtotal { get; set; }
        public double Freight { get; set; }
        public double Discounts { get; set; }
        public double Total { get; set; }
        public string Currency { get; set; }
        public string RawPaymentStatus { get; set; }
        public bool IsPaid { get; set; }
        public string CustomerId { get; set; }
        public string CustomerEmail { get; set; }
        public bool? OptIn { get; set; }
        public List<OrderItem> Items { get; set; }
        public List<PaymentMethod> PaymentMethods { get; set; }
        public List<DeliveryMethod> DeliveryMethods { get; set; }
        public DeliveryAddress DeliveryAddress { get; set; }
    }

    // Middleware para validar a API Key
    public class ApiKeyMiddleware
    {
        private readonly RequestDelegate _next;
        private const string API_KEY = "sua_api_key_aqui"; // Em produção, use configurações do aplicativo

        public ApiKeyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            // Bypass para healthz
            if (string.Equals(context.Request.Path, "/api/v2/healthz", StringComparison.OrdinalIgnoreCase))
            {
                await _next(context);
                return;
            }
            if (!context.Request.Headers.TryGetValue("X-Impulse-Key", out var extractedApiKey))
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsJsonAsync(new { error = "unauthorized", message = "X-Impulse-Key is missing or invalid" });
                return;
            }

            if (!API_KEY.Equals(extractedApiKey))
            {
                context.Response.StatusCode = 401;
                await context.Response.WriteAsJsonAsync(new { error = "unauthorized", message = "X-Impulse-Key is missing or invalid" });
                return;
            }

            await _next(context);
        }
    }

    // Classe principal
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers()
                .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
                    options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
                });
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            // Aplicar middleware de validação da API Key (exceto /healthz)
            app.UseMiddleware<ApiKeyMiddleware>();

            app.UseEndpoints(endpoints =>
            {
                // Healthz (sem autenticação)
                endpoints.MapGet("/api/v2/healthz", async context =>
                {
                    await context.Response.WriteAsJsonAsync(new { status = "ok" });
                });

                // Endpoint de categorias
                endpoints.MapGet("/api/v2/categories", async context =>
                {
                    // Paginação: offset/limit (padrão 50, máx 100)
                    int offset = 0;
                    int limit = 50;
                    int.TryParse(context.Request.Query["offset"], out offset);
                    int.TryParse(context.Request.Query["limit"], out limit);
                    if (limit <= 0) limit = 50;
                    if (limit > 100) limit = 100;
                    if (offset < 0) offset = 0;

                    // Simulação de dados
                    var allCategories = new List<ProductCategory>
                    {
                        new ProductCategory { Id = "1", Name = "Eletrônicos" },
                        new ProductCategory { Id = "2", Name = "Roupas" },
                        new ProductCategory { Id = "3", Name = "Acessórios" }
                    };

                    // Aplicar paginação e envelope
                    var data = allCategories.Skip(offset).Take(limit).ToList();
                    var count = data.Count;
                    var total = allCategories.Count;
                    var hasNext = (offset + count) < total;
                    await context.Response.WriteAsJsonAsync(new
                    {
                        data,
                        total,
                        offset,
                        limit,
                        count,
                        hasNext,
                        nextOffset = hasNext ? offset + count : (int?)null
                    });
                });

                // Endpoint de marcas
                endpoints.MapGet("/api/v2/brands", async context =>
                {
                    int offset = 0;
                    int limit = 50;
                    int.TryParse(context.Request.Query["offset"], out offset);
                    int.TryParse(context.Request.Query["limit"], out limit);
                    if (limit <= 0) limit = 50;
                    if (limit > 100) limit = 100;
                    if (offset < 0) offset = 0;

                    // Simulação de dados
                    var allBrands = new List<Brand>
                    {
                        new Brand { Id = "1", Name = "Samsung" },
                        new Brand { Id = "2", Name = "Apple" },
                        new Brand { Id = "3", Name = "Nike" }
                    };

                    var data = allBrands.Skip(offset).Take(limit).ToList();
                    var count = data.Count;
                    var total = allBrands.Count;
                    var hasNext = (offset + count) < total;
                    await context.Response.WriteAsJsonAsync(new
                    {
                        data,
                        total,
                        offset,
                        limit,
                        count,
                        hasNext,
                        nextOffset = hasNext ? offset + count : (int?)null
                    });
                });

                // Endpoint de produtos
                endpoints.MapGet("/api/v2/products", async context =>
                {
                    int offset = 0;
                    int limit = 50;
                    int.TryParse(context.Request.Query["offset"], out offset);
                    int.TryParse(context.Request.Query["limit"], out limit);
                    if (limit <= 0) limit = 50;
                    if (limit > 100) limit = 100;
                    if (offset < 0) offset = 0;

                    // Simulação de dados
                    var allProducts = new List<Product>
                    {
                        new Product
                        {
                            ProductId = "1",
                            Url = "https://exemplo.com/produto/1",
                            Brand = new Brand { Id = "1", Name = "Samsung" },
                            Category = new List<ProductCategory> { new ProductCategory { Id = "1", Name = "Eletrônicos" } },
                            Variants = new List<Variant> { new Variant { Sku = "SKU-1A", Name = "Variante A", Price = 50.0, Stock = 10, Available = true } }
                        },
                        new Product
                        {
                            ProductId = "2",
                            Url = "https://exemplo.com/produto/2",
                            Brand = new Brand { Id = "2", Name = "Apple" },
                            Category = new List<ProductCategory> { new ProductCategory { Id = "1", Name = "Eletrônicos" } },
                            Variants = new List<Variant> { new Variant { Sku = "SKU-2A", Name = "Variante A", Price = 75.0, Stock = 5, Available = true } }
                        }
                    };

                    var data = allProducts.Skip(offset).Take(limit).ToList();
                    var count = data.Count;
                    var total = allProducts.Count;
                    var hasNext = (offset + count) < total;
                    await context.Response.WriteAsJsonAsync(new
                    {
                        data,
                        total,
                        offset,
                        limit,
                        count,
                        hasNext,
                        nextOffset = hasNext ? offset + count : (int?)null
                    });
                });

                // Endpoint de pedidos
                endpoints.MapGet("/api/v2/orders", async context =>
                {
                    int offset = 0;
                    int limit = 50;
                    int.TryParse(context.Request.Query["offset"], out offset);
                    int.TryParse(context.Request.Query["limit"], out limit);
                    if (limit <= 0) limit = 50;
                    if (limit > 100) limit = 100;
                    if (offset < 0) offset = 0;

                    // Filtro obrigatório de data: start_date/end_date (YYYY-MM-DD)
                    string start = context.Request.Query["start_date"];
                    string end = context.Request.Query["end_date"];
                    if (string.IsNullOrEmpty(start) || string.IsNullOrEmpty(end))
                    {
                        context.Response.StatusCode = 400;
                        await context.Response.WriteAsJsonAsync(new { error = "bad_request", message = "start_date and end_date are required in YYYY-MM-DD format" });
                        return;
                    }
                    if (!DateTime.TryParse(start + "T00:00:00Z", out DateTime startDate) || !DateTime.TryParse(end + "T23:59:59Z", out DateTime endDate))
                    {
                        context.Response.StatusCode = 400;
                        await context.Response.WriteAsJsonAsync(new { error = "bad_request", message = "start_date and end_date must be in YYYY-MM-DD format" });
                        return;
                    }

                    // Simulação de dados
                    var allOrders = new List<Order>
                    {
                        new Order
                        {
                            Platform = "SuaPlataforma",
                            OrderId = "12345",
                            Date = new DateTime(2023, 6, 1, 10, 0, 0, DateTimeKind.Utc),
                            Subtotal = 100.0,
                            Freight = 10.0,
                            Discounts = 5.0,
                            Total = 105.0,
                            Currency = "BRL",
                            RawPaymentStatus = "Pago",
                            IsPaid = true,
                            CustomerId = "1",
                            CustomerEmail = "[email protected]",
                            Items = new List<OrderItem>
                            {
                                new OrderItem
                                {
                                    ProductId = "1",
                                    Categories = new List<ProductCategory> { new ProductCategory { Id = "1", Name = "Eletrônicos" } },
                                    Sku = "SKU-1A",
                                    Name = "Produto 1",
                                    Brand = "Samsung",
                                    Price = 50.0,
                                    Quantity = 2,
                                    Url = "https://exemplo.com/produto/1",
                                    ImageUrl = "https://exemplo.com/img/1.jpg"
                                }
                            },
                            PaymentMethods = new List<PaymentMethod>
                            {
                                new PaymentMethod { Type = "credit_card", Amount = 105.0 }
                            },
                            DeliveryMethods = new List<DeliveryMethod>
                            {
                                new DeliveryMethod { Type = "standard", Amount = 10.0 }
                            },
                            DeliveryAddress = new DeliveryAddress
                            {
                                City = "São Paulo",
                                AddressLine2 = "Apto 101",
                                Neighborhood = "Centro",
                                AddressNumber = "123",
                                State = "SP",
                                AddressLine1 = "Rua Principal",
                                PostalCode = "01000-000",
                                Country = "BR"
                            }
                        }
                    };

                    // Filtrar por data (inclusivo)
                    allOrders = allOrders.Where(o => o.Date >= startDate && o.Date <= endDate).ToList();

                    // Aplicar paginação e envelope
                    var data = allOrders.Skip(offset).Take(limit).ToList();
                    var count = data.Count;
                    var total = allOrders.Count;
                    var hasNext = (offset + count) < total;
                    await context.Response.WriteAsJsonAsync(new
                    {
                        data,
                        total,
                        offset,
                        limit,
                        count,
                        hasNext,
                        nextOffset = hasNext ? offset + count : (int?)null
                    });
                });
            });
        }
    }

    // Programa principal
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Explicação

Este exemplo implementa uma API REST em C# com ASP.NET Core que atende aos requisitos do padrão Flowbiz. A implementação inclui:

  1. Modelos de dados para representar os objetos do domínio (Order, Product, etc.)
  2. Middleware de autenticação para validar a API Key
  3. Endpoints para listar categorias, marcas, produtos e pedidos
  4. Suporte à paginação em todos os endpoints
  5. Filtro de data no endpoint de pedidos

Para usar este exemplo, você precisará:

  1. Substituir sua_api_key_aqui pela sua chave de API real
  2. Implementar a lógica para buscar dados do seu banco de dados ou sistema de armazenamento
  3. Adaptar as estruturas de dados conforme necessário para seu caso de uso específico

Configuração do Projeto

Para criar um novo projeto ASP.NET Core e implementar este exemplo:

dotnet new web -n FlowbizApi
cd FlowbizApi

Substitua o conteúdo do arquivo Program.cs e Startup.cs pelo código fornecido acima.

Executando o Exemplo

Para executar este exemplo:

dotnet run

O servidor será iniciado na porta 5000 (HTTP) e 5001 (HTTPS) e estará pronto para receber requisições.

Testando a API

Você pode testar a API usando ferramentas como Postman, cURL ou qualquer cliente HTTP. Lembre-se de incluir o header X-Impulse-Key com o valor correto em todas as requisições.

Exemplo de requisição:

curl -X GET "http://localhost:5000/api/v2/categories" \
     -H "X-Impulse-Key: sua_api_key_aqui"