01Summary
End-to-end e-commerce platform for a Georgian embroidery business. Catalog, custom-order designer, real payments, and a live store at dressfield.ge.
02Problem
A custom-embroidery business needed an online storefront where customers could not just browse, but actively design custom orders — uploading artwork, previewing it on garments, and paying with the local Bank of Georgia gateway.
03Approach
- 01Designed and shipped end-to-end — architecture, API, frontend, deployment, DNS, SSL.
- 02Built the frontend with Next.js 15, Tailwind, and shadcn/ui for a fast, responsive shopping experience with server-side rendering.
- 03Implemented a real-time custom-order designer using Fabric.js — customers upload artwork and preview it live on products before ordering.
- 04Developed a RESTful backend with ASP.NET Core 8 and MySQL handling product catalog, authentication, and order management.
- 05Integrated Bank of Georgia iPay for live card payments.
- 06Deployed backend on Azure, frontend on Hostinger; managed environment configuration end-to-end.
04Highlights
- Live in production at dressfield.ge
- Real-time canvas-based custom-order designer
- Bank of Georgia iPay integration
- Server-side rendered storefront
- Self-managed Azure + Hostinger deployment
05Stack
Next.js 15TypeScriptTailwind CSSshadcn/uiFabric.jsASP.NET Core 8MySQLAzureBOG iPay
06Payment Integration
DressField uses Bank of Georgia's iPay REST API for payments. The integration handles OAuth2 token exchange, hosted-checkout session creation, and signed webhook verification — all behind a clean interface so the rest of the app never knows which provider is wired in.
Payment flow
01
User submits checkout
POST /api/orders → order created, status: pending
│
02
API fetches BOG access token
POST oauth2.bog.ge → client_credentials grant → Bearer token
│
03
Create BOG payment session
POST api.bog.ge/payments/v1/ecommerce/orders → { id, redirect_url }
│
04
User redirected to BOG hosted checkout
Customer completes card payment on BOG's secure page
│
05
BOG POSTs signed callback
POST /api/payments/callback?key={orderKey} · Callback-Signature header
│
06
Verify RSA-SHA256 signature
Reject if invalid — never trust unverified callback bodies
│
07
Re-verify order status via GET
GET /ecommerce/orders/{id} → confirm status: completed
│
08
Order marked paid · customer redirected
/order-confirmation or /order-failed
Key code
Core/Interfaces/IPaymentService.csC#
// Interface lives in Core — Infrastructure is the only layer that knows about BOGpublic interface IPaymentService { Task<PaymentSessionResult> CreateSessionAsync( int orderId, decimal amount, string orderKey, string description); Task<PaymentVerificationResult> VerifyCallbackAsync( string bogOrderId); } public record PaymentSessionResult( bool Success, string? RedirectUrl, string? BogOrderId, string? ErrorMessage); public record PaymentVerificationResult( bool IsApproved, string BogOrderId, string? TransactionId, string Status);
Infrastructure/Services/BogIPayService.cs — OAuth2 tokenC#
// client_credentials grant — no user context, machine-to-machineprivate async Task<string> GetAccessTokenAsync() { var credentials = Convert.ToBase64String( Encoding.UTF8.GetBytes($"{_clientId}:{_clientSecret}")); using var req = new HttpRequestMessage( HttpMethod.Post, _tokenUrl); req.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); req.Content = new FormUrlEncodedContent( new Dictionary<string, string> { ["grant_type"] = "client_credentials" }); var res = await _http.SendAsync(req); var raw = await res.Content.ReadAsStringAsync(); if (!res.IsSuccessStatusCode) throw new InvalidOperationException( $"BOG token request failed ({res.StatusCode}): {raw}"); using var doc = JsonDocument.Parse(raw); return doc.RootElement .GetProperty("access_token") .GetString() ?? throw new InvalidOperationException( "BOG token response missing access_token."); }
API/Controllers/PaymentsController.cs — Webhook verificationC#
// BOG signs every callback with RSA-SHA256 — reject anything that doesn't verifyprivate bool IsValidSignature(string rawBody, string? signature) { if (string.IsNullOrWhiteSpace(signature)) return false; try { using var rsa = RSA.Create(); rsa.ImportFromPem(_callbackPublicKeyPem); // BOG's public key var payloadBytes = Encoding.UTF8.GetBytes(rawBody); var signatureBytes = Convert.FromBase64String(signature.Trim()); return rsa.VerifyData( payloadBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); } catch (Exception ex) { _logger.LogError(ex, "Signature verification failed."); return false; } } // Callback always returns 200 OK — BOG retries on any other status [HttpPost("callback")] public async Task<IActionResult> Callback([FromQuery] string? key) { var rawBody = await new StreamReader(Request.Body).ReadToEndAsync(); var signature = Request.Headers["Callback-Signature"].FirstOrDefault(); if (!IsValidSignature(rawBody, signature)) return Ok(); // silent reject // route to custom-order or regular-order handler if (key?.StartsWith("c-") == true) await _customOrders.HandlePaymentCallbackAsync(bogOrderId, key); else await _orders.HandlePaymentCallbackAsync(bogOrderId, key); return Ok(); }
Technical notes
- →Callback always returns 200 — BOG retries on non-200, so errors are logged and swallowed
- →RSA-SHA256 signature verified before any database write — forged callbacks do nothing
- →Order status re-verified via GET after callback — never trust callback body alone
- →IPaymentService interface lets dev/test run against MockPaymentService without hitting BOG
- →c- prefix on orderKey routes custom orders to a separate handler without an extra endpoint
- →All credentials injected via IConfiguration — no secrets in source, ready for Azure Key Vault