Process Sep 14, 2024 12 min read

Gioielleria Bonanno — a Full-Stack Development Case Study

Full-stack development case study for a luxury watch retailer: AI-powered product descriptions with OpenAI, Docker containerized development environment, Mailchimp newsletter automation, and a Laravel/Nova CRM system. 15+ months of backend development across WordPress/WooCommerce and Laravel with detailed code examples.

Lushano Perera
Lushano Perera
Author

Executive Summary

AspectDetails
ClientGioielleria Bonanno – Luxury Watch & Jewelry Retailer (Rome, Italy)
ScopeE-commerce Platform + Internal CRM System
RoleBackend & Full-Stack Developer
DurationSeptember 2024 – Present (15+ months)
Commits100+ commits across both projects

I developed and maintained two interconnected systems for a luxury watch retailer: a high-traffic WooCommerce e-commerce platform and a Laravel-based CRM system for customer relationship management. Key achievements include building an AI-powered product description generator, architecting a containerized development environment, implementing newsletter automation, and creating a comprehensive customer management system.


Technology Stack Overview


PROJECT 1: E-COMMERCE PLATFORM

1.1 AI-Powered Product Description Generator

I developed a custom WordPress plugin that leverages OpenAI’s GPT models to generate technical, collector-focused descriptions for luxury watches. The plugin integrates seamlessly with WooCommerce and ACF fields.

Technical Highlights

  • Singleton Pattern with lazy initialization for optimal performance
  • WPML Integration for multilingual support (IT/EN/FR/DE)
  • Duplicate Detection via MD5 hashing with automatic regeneration
  • Vanilla JavaScript admin interface (jQuery-free for smaller footprint)
  • Database Versioning System for smooth upgrades

Core Architecture

/**
* Plugin: AI Watch Description Generator
* Singleton pattern ensures single instantiation
*/
class AI_Watch_Description_Generator {

private static $instance = null;

public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}

private function __construct() {
$this->init_hooks();
}

private function init_hooks() {
// Admin menu
add_action('admin_menu', array($this, 'add_admin_menu'));

// Context-aware asset loading - only on product pages
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));

// Inject into WooCommerce product edit page
add_action('woocommerce_product_options_general_product_data',
array($this, 'add_generate_button'));

// AJAX handler with nonce verification
add_action('wp_ajax_aiwd_generate_description',
array($this, 'handle_generate_description'));

// Lifecycle hooks
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
}
}

// Initialize at plugins_loaded for optimal timing
add_action('plugins_loaded', array('AI_Watch_Description_Generator', 'get_instance'));

OpenAI Integration with Prompt Engineering

private function generate_description_with_openai($product_data, $variation = false) {
$api_key = get_option('aiwd_openai_key');
$prompt = $this->build_prompt($product_data, $variation);

// WPML language detection
$current_lang = defined('ICL_LANGUAGE_CODE') ? ICL_LANGUAGE_CODE : 'it';

// Multilingual system messages for collector-focused descriptions
$system_messages = array(
'it' => 'Sei un orologiaio master e consulente per collezionisti.
Crei descrizioni tecniche di massimo 55 parole usando
terminologia specialistica orologiera.',
'en' => 'You are a master watchmaker and collector consultant.
Create technical descriptions of maximum 55 words using
specialized horological terminology.',
// ... FR, DE support
);

$response = wp_remote_post('https://api.openai.com/v1/chat/completions', array(
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json'
),
'body' => json_encode(array(
'model' => get_option('aiwd_model', 'gpt-3.5-turbo'),
'messages' => array(
array('role' => 'system', 'content' => $system_messages[$current_lang]),
array('role' => 'user', 'content' => $prompt)
),
'max_tokens' => 80, // Token optimization
'temperature' => $variation ? 0.8 : 0.7 // Higher for variations
)),
'timeout' => 30
));

// Parse and clean response
$content = trim($data['choices'][0]['message']['content']);
return trim($content, '"\''); // Remove surrounding quotes
}

Duplicate Detection System

private function is_duplicate_description($description, $current_product_id) {
global $wpdb;

// Layer 1: Fast hash-based check
$hash = md5(strtolower(str_replace(' ', '', $description)));
$table_name = $wpdb->prefix . 'aiwd_descriptions';

$exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $table_name WHERE hash = %s AND product_id != %d",
$hash,
$current_product_id
));

if ($exists > 0) return true;

// Layer 2: Semantic similarity check in posts
$similar = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
WHERE p.post_type = 'product' AND p.ID != %d
AND (p.post_excerpt LIKE %s OR pm.meta_value LIKE %s)",
$current_product_id,
'%' . $wpdb->esc_like(substr($description, 0, 50)) . '%',
'%' . $wpdb->esc_like(substr($description, 0, 50)) . '%'
));

return $similar > 0;
}

Vanilla JavaScript Admin Interface

/**
* AI Watch Description Generator - Admin JavaScript
* No jQuery dependency - pure ES6+
*/
(function() {
'use strict';

// Custom AJAX helper replacing jQuery.ajax()
function ajaxRequest(options) {
const xhr = new XMLHttpRequest();
const formData = new FormData();

Object.keys(options.data).forEach(key => {
formData.append(key, options.data[key]);
});

xhr.open(options.type || 'POST', options.url);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');

xhr.onload = function() {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (options.success) options.success(response);
} else {
if (options.error) options.error(xhr, xhr.status);
}
if (options.complete) options.complete();
};

xhr.send(formData);
}

// Animation helper for visual feedback
function animateBackgroundColor(element, startColor, endColor, duration) {
element.style.backgroundColor = startColor;
element.style.transition = `background-color ${duration}ms`;
setTimeout(() => element.style.backgroundColor = endColor, 10);
}

document.addEventListener('DOMContentLoaded', function() {
const generateBtn = document.getElementById('aiwd-generate-btn');

generateBtn.addEventListener('click', function(e) {
e.preventDefault();
this.disabled = true;

ajaxRequest({
url: aiwd_ajax.ajax_url,
data: {
action: 'aiwd_generate_description',
product_id: this.dataset.productId,
nonce: aiwd_ajax.nonce // Security
},
success: function(response) {
if (response.success) {
generatedDesc.value = response.data.description;
animateBackgroundColor(generatedDesc, '#ffffcc', '#fff', 1000);
}
}
});
});

// Auto-save to localStorage with debouncing
let autoSaveTimer;
generatedDesc.addEventListener('input', function() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
localStorage.setItem('aiwd_draft_' + productId, this.value);
}, 1000);
});
});
})();

1.2 Docker Development Environment

I architected a comprehensive containerized development stack that mirrors production while adding developer-friendly features like automatic asset proxying from production.

Architecture Overview

Docker Compose Configuration

# docker-compose.yml
services:
# MariaDB Database
mariadb:
image: mariadb:10.11
container_name: gioielleriabonanno_mariadb
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: "password"
MYSQL_DATABASE: "tucrkdmuqj"
MYSQL_USER: "tucrkdmuqj"
MYSQL_PASSWORD: "U4b8cG5gzu"
volumes:
- mariadb_data:/var/lib/mysql
- ./docker/mariadb/init:/docker-entrypoint-initdb.d
ports:
- "3306:3306"
networks:
- gioielleriabonanno_network

# Redis for Object Cache
redis:
image: redis:7-alpine
container_name: gioielleriabonanno_redis
restart: unless-stopped
ports:
- "6379:6379"

# PHP-FPM 8.4 with all required extensions
php:
build:
context: ./docker/php
dockerfile: Dockerfile
container_name: gioielleriabonanno_php
depends_on:
- mariadb
- redis
volumes:
- .:/var/www/html:delegated # Optimized for macOS
- ./docker/php/conf.d/custom.ini:/usr/local/etc/php/conf.d/custom.ini
extra_hosts:
- "gioielleriabonanno.it.local:host-gateway"

# NGINX with Proxy Cache and Production Asset Proxying
nginx:
build:
context: ./docker/nginx
dockerfile: Dockerfile
container_name: gioielleriabonanno_nginx
ports:
- "80:80"
- "443:443"
volumes:
- .:/var/www/html:delegated
- ./docker/nginx/conf.d:/etc/nginx/conf.d
- ./docker/nginx/cache:/var/cache/nginx

# Elasticsearch for Search
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data
ports:
- "9200:9200"

# Mailhog for Email Testing
mailhog:
image: mailhog/mailhog:latest
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI

volumes:
mariadb_data:
elasticsearch_data:

networks:
gioielleriabonanno_network:
driver: bridge

NGINX Configuration with Smart Asset Proxying

# Proxy cache for production assets
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m
inactive=7d use_temp_path=off;

resolver 8.8.8.8 8.8.4.4 valid=300s;

server {
listen 80;
server_name gioielleriabonanno.it.local www.gioielleriabonanno.it.local;
root /var/www/html;

client_max_body_size 100M;

# Gzip compression
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css text/javascript application/json
application/javascript image/svg+xml;

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# Static assets with production fallback
location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri @production_assets;
}

# KEY FEATURE: Auto-proxy missing uploads from production
location @production_uploads {
proxy_pass https://www.gioielleriabonanno.it$request_uri;
proxy_set_header Host www.gioielleriabonanno.it;
proxy_ssl_server_name on;
proxy_cache STATIC;
proxy_cache_valid 200 30d;
proxy_cache_use_stale error timeout http_500 http_502 http_503;
add_header X-Cache-Status $upstream_cache_status;
}

# WordPress rewrite rules
location / {
try_files $uri $uri/ /index.php?$args;
}

# PHP-FPM with optimized buffers
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_buffer_size 128k;
fastcgi_buffers 256 16k;
fastcgi_read_timeout 300;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

# Block xmlrpc.php for security
location = /xmlrpc.php {
deny all;
}
}

1.3 Mailchimp Integration & Newsletter Automation

I implemented a complete newsletter system with Mailchimp API v3 integration, dynamic product content generation, and UTM parameter tracking for marketing analytics.

Newsletter Content Generation

<?php
// mailchimp_data/content.php
// Dynamic newsletter content from WooCommerce products

setlocale(LC_MONETARY, 'it_IT'); // Italian locale for pricing

$args = [
'post_type' => 'product',
'no_found_rows' => 1, // Performance: skip pagination count
'orderby' => [
'post_date' => 'DESC',
'price_clause' => 'ASC',
],
'posts_per_page' => 50,
'tax_query' => [
[
'taxonomy' => 'product_group',
'field' => 'slug',
'terms' => 'modern', // Only modern watches
],
],
'meta_query' => [
// Exclude private negotiation items
[
'key' => 'watch_private_negotiation',
'value' => '1',
'compare' => '!='
],
// Only in-stock products
[
'key' => '_stock_status',
'value' => 'instock',
'compare' => '='
],
// No backorders
[
'key' => '_backorders',
'value' => 'no',
],
// Must have price
[
'key' => '_regular_price',
'compare' => 'EXISTS'
]
]
];

$query = new WP_Query($args);

while ($query->have_posts()) {
$query->the_post();

$product_data = [
'image' => get_the_post_thumbnail_url(get_the_ID(), 'square-206'),
'title' => strtoupper(get_the_title()),
'url' => get_permalink(),
'ref' => get_field('watch_reference'),
'year' => get_field('watch_production_year'),
'price' => numfmt_format_currency(
numfmt_create('it_IT', NumberFormatter::CURRENCY),
get_post_meta(get_the_ID(), '_regular_price', true),
"EUR"
),
'condition' => get_field('watch_status'),
];

// Generate HTML for email template...
}

Contact Form 7 with UTM Tracking

<?php
// app/cf7.php - Custom CF7 form tags for marketing tracking

// Register custom form tag for UTM parameters
wpcf7_add_form_tag('custom_data', function($tag) {
$tag = new WPCF7_FormTag($tag);

// Map form field names to URL parameters
$utm_params = [
'utm_source' => $_GET['utm_source'] ?? '',
'utm_medium' => $_GET['utm_medium'] ?? '',
'utm_campaign' => $_GET['utm_campaign'] ?? '',
'utm_term' => $_GET['utm_term'] ?? '',
'utm_content' => $_GET['utm_content'] ?? '',
];

if (isset($utm_params[$tag->name])) {
return sprintf(
'<input type="hidden" name="%s" value="%s" />',
esc_attr($tag->name),
esc_attr($utm_params[$tag->name])
);
}

return '';
});

// Usage in CF7 form:
// [custom_data utm_source]
// [custom_data utm_medium]
// [custom_data utm_campaign]

1.4 WooCommerce Backend Architecture

I developed extensive WooCommerce customizations including product group-specific templates, custom REST API endpoints, FacetWP integration for filtering, and smart caching strategies.

Product Group-Specific Functions

<?php
// app/woocommerce/woocommerce-template-functions.php

// Dynamic product loop for different watch categories
function woocommerce_single_product_modern_get_watch_information() {
if (has_term('modern', 'product_group')) {
echo \App\template('woocommerce/single-product/modern/watch-information');
}
}

function woocommerce_single_product_vintage_main_information() {
if (has_term('vintage', 'product_group')) {
echo '<div>';
echo \App\template('woocommerce/single-product/vintage/main-information');
echo '</div>';
echo '<div>';
echo \App\template('woocommerce/single-product/vintage/year');
echo \App\template('woocommerce/single-product/price');
echo '</div>';
echo \App\template('woocommerce/single-product/vintage/availability');
}
}

// Smart related products - same brand AND same product group
function woocommerce_single_product_custom_related_products($args) {
global $product;

$current_group = wc_get_product_terms(
$product->get_id(), 'product_group', ['fields' => 'slugs']
);
$current_category = wc_get_product_terms(
$product->get_id(), 'product_cat', ['fields' => 'slugs']
);

return [
'post_type' => 'product',
'post_status' => 'publish',
'numberposts' => 10,
'exclude' => [$product->get_id()],
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'product_group',
'field' => 'slug',
'terms' => $current_group,
],
[
'taxonomy' => 'product_cat',
'field' => 'slug',
'terms' => $current_category,
]
]
];
}

// Conditional purchaseability based on ACF fields
function woocommerce_single_product_is_purchasable($is_purchasable) {
$is_purchasable = get_field('field_61532315ac6ee'); // is_purchasable
$is_private = get_field('field_60e1885d89967'); // private_negotiation
$is_available = get_field('field_60e187dcc951d'); // availability

if (is_product() &&
has_term(['vintage', 'modern'], 'product_group') &&
(!$is_purchasable || $is_private || $is_available)) {
return false;
}

return $is_purchasable;
}

FacetWP AJAX Pagination Integration

<?php
// Private negotiation filter with FacetWP AJAX support

function woocommerce_archive_pre_get_posts($query) {
if ($query->is_main_query() && !is_admin()) {
// Filter by private negotiation when URL parameter is set
if (isset($_GET['only_private_negotiation']) &&
$_GET['only_private_negotiation'] === 'true') {

$meta_query = $query->get('meta_query') ?: [];
$meta_query[] = [
'key' => 'watch_private_negotiation',
'value' => '1',
'compare' => '='
];
$query->set('meta_query', $meta_query);
}
}
}

// Ensure filter works with FacetWP AJAX "Load more"
function facetwp_private_negotiation_filter($query_args) {
if (isset($_GET['only_private_negotiation']) &&
$_GET['only_private_negotiation'] === 'true') {

$meta_query = $query_args['meta_query'] ?? [];
$meta_query[] = [
'key' => 'watch_private_negotiation',
'value' => '1',
'compare' => '='
];
$query_args['meta_query'] = $meta_query;
}
return $query_args;
}
add_filter('facetwp_query_args', 'facetwp_private_negotiation_filter', 10, 1);

PROJECT 2: LARAVEL CRM SYSTEM

2.1 System Architecture

I built a complete CRM system using Laravel 10 and Nova 4 admin panel for managing customer relationships, product sales, and compliance documentation.


2.2 Customer Management with Conditional Forms

A sophisticated Nova resource with dynamic field visibility based on customer type (Individual vs Company).

<?php
// app/Nova/Customer.php

namespace App\Nova;

use Laravel\Nova\Fields\{ID, Text, Email, Select, Image, Date, HasMany, Heading};
use Laravel\Nova\Fields\FormData;
use Laravel\Nova\Panel;

class Customer extends Resource
{
public static $model = \App\Models\Customer::class;
public static $search = ['id', 'full_name', 'company_name'];

public function fields(NovaRequest $request)
{
return [
// Customer type selector with localized labels
Select::make('Tipo cliente', 'customer_type')
->options([
'Individual' => 'Privato',
'Company' => 'Azienda',
])
->displayUsingLabels()
->rules('required')
->sortable(),

// Organized panels for better UX
Panel::make('Dettagli cliente', $this->customerDetailsFields()),
Panel::make('Informazioni di spedizione', $this->shippingFields()),

// Related products
HasMany::make('Prodotti', 'products', Product::class),
];
}

protected function customerDetailsFields()
{
return [
// CONDITIONAL VISIBILITY: Company fields only show when type = Company
Heading::make('Dati azienda')
->hide()
->dependsOn(['customer_type'], function ($field, NovaRequest $request, FormData $formData) {
if ($formData->customer_type === 'Company') {
$field->show();
}
}),

Text::make('Nome azienda', 'company_name')
->hide()
->dependsOn('customer_type', function (Text $field, NovaRequest $request, FormData $formData) {
if ($formData->customer_type === 'Company') {
$field->show();
}
})
->sortable(),

Text::make('P.IVA/VAT', 'company_vat')
->hide()
->dependsOn('customer_type', function (Text $field, NovaRequest $request, FormData $formData) {
if ($formData->customer_type === 'Company') {
$field->show();
}
})
->hideFromIndex(),

// AML Compliance: Document ID images for anti-money laundering
Heading::make('Dati individuo o amministratore (se azienda)'),

Text::make('Nome completo', 'full_name')->sortable(),
Text::make('N. documento d\'identità', 'document_id_number')->hideFromIndex(),

// Document images for compliance
Image::make('Immagine documento 1', 'document_id_image_1')
->disk('public')
->path('products')
->rules('image', 'max:1024')
->hideFromIndex(),

Image::make('Immagine documento 2', 'document_id_image_2')
->disk('public')
->path('products')
->hideFromIndex(),

Date::make('Data di nascita', 'birth_date')->hideFromIndex(),
Email::make('Email', 'email'),
Text::make('Telefono', 'phone'),
];
}

// Custom title with fallback
public function title()
{
return $this->display_name
? $this->display_name . ' (ID: ' . $this->id . ')'
: 'Customer #' . $this->id;
}
}

2.3 Eloquent Models with Accessors

<?php
// app/Models/Customer.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Customer extends Model
{
use HasFactory;

protected $casts = [
'birth_date' => 'date', // Automatic date casting
];

// One-to-many relationship: Customer -> Products
public function products()
{
return $this->hasMany(Product::class);
}

// Accessor for flexible display naming
// Works with both Individual and Company customers
public function getDisplayNameAttribute()
{
return $this->full_name ?? $this->company_name ?? 'Unknown Customer';
}
}
<?php
// app/Models/Product.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
use HasFactory;

protected $casts = [
'sell_date' => 'date',
];

// Many-to-one relationship: Product -> Customer
public function customer()
{
return $this->belongsTo(Customer::class);
}
}

2.4 Database Schema Design

<?php
// database/migrations/2024_12_04_create_customers_table.php

Schema::create('customers', function (Blueprint $table) {
$table->id();

// Customer type discriminator
$table->enum('customer_type', ['Individual', 'Company']);

// COMPANY FIELDS
$table->string('company_name')->nullable();
$table->string('company_vat')->nullable(); // VAT number
$table->string('company_address')->nullable();
$table->string('company_city')->nullable();
$table->string('company_postal_code')->nullable();
$table->string('company_country')->nullable();
$table->string('company_email')->nullable();
$table->string('company_phone')->nullable();

// INDIVIDUAL FIELDS
$table->string('full_name')->nullable();
$table->string('document_id_number')->nullable(); // Passport/ID
$table->string('document_id_image_1')->nullable(); // AML compliance
$table->string('document_id_image_2')->nullable(); // AML compliance
$table->date('birth_date')->nullable();
$table->string('birth_place')->nullable();
$table->string('address')->nullable();
$table->string('city')->nullable();

// SHIPPING ADDRESS (separate from billing)
$table->string('shipping_address')->nullable();
$table->string('shipping_city')->nullable();
$table->string('shipping_postal_code')->nullable();
$table->string('shipping_country')->nullable();

$table->timestamps();
});
<?php
// database/migrations/2024_12_04_create_products_table.php

Schema::create('products', function (Blueprint $table) {
$table->id();

// Product details
$table->string('product_name');
$table->text('product_description')->nullable();
$table->string('product_image_1')->nullable();
$table->string('product_image_2')->nullable();

// Sales information
$table->string('sell_referent')->nullable(); // Staff member
$table->date('sell_date')->nullable();
$table->decimal('total_amount', 10, 2)->nullable(); // High-precision EUR
$table->string('payment')->nullable(); // Payment method

// Foreign key with cascade
$table->unsignedBigInteger('customer_id')->nullable();
$table->foreign('customer_id')
->references('id')
->on('customers')
->onDelete('cascade');

$table->timestamps();
});

Technical Highlights

Code Quality Patterns Used

PatternImplementationBenefit
SingletonAI Plugin main classSingle instance, optimal memory
AccessorLaravel Customer modelClean display logic separation
Conditional VisibilityNova FormDataDynamic form rendering
Transient CachingWordPress queriesReduced database load
Prepared StatementsAll SQL queriesSQL injection prevention

Security Implementations

// Nonce verification for AJAX
if (!wp_verify_nonce($_POST['nonce'], 'aiwd_generate_nonce')) {
wp_die('Security check failed');
}

// Capability checks
if (!current_user_can('generate_aiwd_descriptions')) {
return;
}

// Prepared statements
$wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM $table WHERE hash = %s AND product_id != %d",
$hash,
$product_id
));

// Output escaping
echo esc_html($watch_reference);
echo esc_attr($product_class);
echo esc_url($product_url);

Performance Optimizations

// Transient caching for expensive queries
$cache_key = 'modern_featured_products';
$products = get_transient($cache_key);

if ($products === false) {
$products = // expensive query
set_transient($cache_key, $products, MONTH_IN_SECONDS);
}

// Cache invalidation on product updates
add_action('woocommerce_update_product', function() {
delete_transient('modern_featured_products');
delete_transient('vintage_latest_products');
});

// Docker: delegated volumes for macOS performance
volumes:
- .:/var/www/html:delegated

Results & Impact

E-Commerce Platform

MetricImpact
Development SetupReduced from hours to minutes with Docker
Product DescriptionsAutomated AI generation saving copywriting time
NewsletterFully automated with smart product filtering
Mobile APIREST endpoints enabling mobile app development
Private SalesSeamless FacetWP filtering for VIP products

CRM System

MetricImpact
Customer TrackingComplete relationship management
AML ComplianceDocument storage for regulatory requirements
Sales AnalyticsReal-time dashboard metrics
Multi-User AccessStaff-level access control

Active Development Period: September 2024 – Present

Written by Lushano Perera

Digital craftsman exploring the intersection of design, technology, and human experience.