Live2025
Next.js 15 / TypeScript / Tailwind CSS

DressField

Full-stack e-commerce platform — shipped, live, taking real orders.

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
  1. 01Designed and shipped end-to-end — architecture, API, frontend, deployment, DNS, SSL.
  2. 02Built the frontend with Next.js 15, Tailwind, and shadcn/ui for a fast, responsive shopping experience with server-side rendering.
  3. 03Implemented a real-time custom-order designer using Fabric.js — customers upload artwork and preview it live on products before ordering.
  4. 04Developed a RESTful backend with ASP.NET Core 8 and MySQL handling product catalog, authentication, and order management.
  5. 05Integrated Bank of Georgia iPay for live card payments.
  6. 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