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.
- RegisterKind Mobile or desktop, callback
http://127.0.0.1:53682/callback(a port your app listens on), permissionsinvoice.read invoice.write. - Sign in through the browserCSHARP
// 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) - List customersCSHARP
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); - Make an invoice, and handle approvalCSHARP
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").