🇺🇸[EN-US] Building a Simple Voucher System for Small Businesses
When I started as a freelancer, one of my first projects was for a small burger shop. The owner wanted a voucher system to reward loyal customers: after collecting five vouchers, customers could claim a free burger. The project needed to be simple, reliable, and tailored to their specific needs. Here's how I approached it.
The Challenge
The main requirements were:
- Generate unique vouchers for customers when they purchase a burger.
- Validate a set of five vouchers to allow for a free burger.
- Keep the system lightweight, as it would run on a single machine.
My Solution
I designed the system using Spring Boot with Thymeleaf to render the front end. Instead of building a complex REST API, I created an intuitive web interface that allows employees to generate and validate vouchers directly.
Key Features
-
Voucher Generation:
- A unique token is generated based on the current date and time.
- The token is stored in a Redis database (for scalability) or in memory (for simplicity).
- A web page with a single button generates a new token.
-
Voucher Validation:
- Employees can input five tokens into a form to verify their validity.
- If all tokens are valid, the system approves the free burger.
-
Simplicity:
- Using Thymeleaf, I avoided the need for a separate frontend framework.
- The system is accessible via any browser and integrates seamlessly with the small business's operations.
Technical Stack
- Backend: Spring Boot
- Frontend: Thymeleaf
- Database: Redis (for token storage and expiration)
- Hosting: A single machine
Code
HTML Templates
Inside the folder resources > templates.
Create 3 files, these are the views of our application.
- index.html - The home page
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Gerenciador de Vouchers</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Bem-vindo ao Sistema de Vouchers</h1>
<ul>
<li><a th:href="@{/vouchers/create}">Gerar Voucher</a></li>
<li><a th:href="@{/vouchers/validate}">Validar Vouchers</a></li>
</ul>
</div>
</body>
</html>
- createToken.html - View to create tokens
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Gerar Token</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Gerar Voucher</h1>
<a href="/">Página Inicial</a>
<form action="/vouchers/create" method="post">
<button type="submit">Gerar Voucher</button>
</form>
<div class="ticket" th:if="${token}">
<p>Seu Voucher:</p>
<h2 th:text="${token}"></h2>
<p>Valido até:</p>
<h3 th:text="${validade}"></h3>
</div>
</div>
</body>
</html>
- validateTokens.html - View to validate tokens
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Validar Vouchers</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<h1>Validar Vouchers</h1>
<a href="/">Página Inicial</a>
<p class="errors" th:if="${erros}" th:text="${erros}"></p>
<form action="/vouchers/validate" method="post">
<label for="token1">Token 1:</label>
<input type="text" id="token1" name="token1" required>
<label for="token2">Token 2:</label>
<input type="text" id="token2" name="token2" required>
<label for="token3">Token 3:</label>
<input type="text" id="token3" name="token3" required>
<label for="token4">Token 4:</label>
<input type="text" id="token4" name="token4" required>
<label for="token5">Token 5:</label>
<input type="text" id="token5" name="token5" required>
<button type="submit">Validar</button>
</form>
<p th:if="${message}" th:text="${message}"></p>
</div>
</body>
</html>
CSS
Inside the folder resources > static
Create a folder CSS and inside that a file called style.css
style.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
color: #333;
}
.container {
max-width: 600px;
margin: 50px auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
h1 {
color: #0073e6;
text-align: center;
}
a {
text-decoration: none;
color: #0073e6;
margin-bottom: 20px;
display: inline-block;
}
a:hover {
text-decoration: underline;
}
button {
background-color: #0073e6;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #005bb5;
}
form {
display: flex;
flex-direction: column;
}
label {
margin: 10px 0 5px;
}
input {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 15px;
}
p {
margin-top: 20px;
font-weight: bold;
color: #4caf50;
}
.errors{
color: red;
}
.ticket {
margin-top: 20px;
padding: 20px;
border: 2px dashed #333;
border-radius: 10px;
background: linear-gradient(135deg, #fdfdfd 25%, #f3f3f3 100%);
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
position: relative;
}
.ticket p {
font-size: 1.2em;
font-weight: bold;
margin: 0;
color: #555;
}
.ticket h2 {
font-size: 2em;
margin: 10px 0 0;
color: #000;
font-family: 'Courier New', Courier, monospace;
}
.ticket::before,
.ticket::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
background: #f9f9f9;
border: 2px solid #333;
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
z-index: 10;
}
.ticket::before {
left: -10px;
}
.ticket::after {
right: -10px;
}