Sign in

An invoicing app (C#)

C# on Windows: customers and invoices, with approvals.

A Windows desktop app in C# (.NET 8) that lists customers and makes sales invoices as the person signed in, with their branch, territory and approval rules.

  1. RegisterKind Mobile or desktop, callback http://127.0.0.1:53682/callback (a port your app listens on), permissions invoice.read invoice.write.
  2. Sign in through the browser
    CSHARP
    // dotnet add package IdentityModel.OidcClient
    var options = new OidcClientOptions {
        Authority = "https://accounts.zilomail.in/oidc",
        ClientId = "zo_app_…",
        Scope = "openid email profile invoice.read invoice.write",
        RedirectUri = "http://127.0.0.1:53682/callback",
        Browser = new SystemBrowser(53682),              // opens the default browser, listens for the callback
    };
    var oidc = new OidcClient(options);
    var login = await oidc.LoginAsync();                 // PKCE and state included
    if (login.IsError) throw new Exception(login.Error);
    tokens.Save(login.AccessToken, login.RefreshToken);  // ProtectedData (DPAPI)
  3. List customers
    CSHARP
    var http = new HttpClient(new RefreshTokenDelegatingHandler(oidc, tokens.Access, tokens.Refresh)) { BaseAddress = new Uri("https://api.zilomail.in/") };
    var page = await http.GetFromJsonAsync<Page<Customer>>("v1/invoice/customers?limit=100&q=kumar");
    foreach (var c in page!.Data) Console.WriteLine($"{c.Code}  {c.Name}  owes {c.Outstanding}");
    
    record Page<T>(List<T> Data, string? Next);
    record Customer(string Id, string Code, string Name, string Outstanding);
  4. Make an invoice, and handle approval
    CSHARP
    var req = new HttpRequestMessage(HttpMethod.Post, "v1/invoice/invoices") {
        Content = JsonContent.Create(new {
            customerId = customer.Id,
            date = DateTime.Today.ToString("yyyy-MM-dd"),
            lines = new[] { new { productId = tea.Id, quantity = 12 } },
        }),
    };
    req.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());   // safe to retry
    var res = await http.SendAsync(req);
    
    if (res.StatusCode == HttpStatusCode.Accepted) {
        var waiting = await res.Content.ReadFromJsonAsync<Pending>();
        ShowBanner($"Sent for approval ({waiting!.Approval.Id})");    // check later: GET v1/approvals/{id}
    } else {
        res.EnsureSuccessStatusCode();
        var invoice = await res.Content.ReadFromJsonAsync<Invoice>();
        ShowBanner($"Invoice {invoice!.Number}: {invoice.Total}");
    }

Lines take the product's rate and base unit unless you send them; tax comes from the branch and the customer's place of supply, exactly as on the invoice form. A 422 carries the form's own message ("Not enough stock of Tea 250 g in Guwahati").