Sistema completo de gestión de ventas y mantenimiento de tanques industriales construido sobre Salesforce, utilizando Sales Cloud, Apex, Lightning Web Components, Flow Builder e integraciones con APIs externas.
- Descripción del Proyecto
- Demostración
- Arquitectura de la Solución
- Modelo de Datos
- Funcionalidades Implementadas
- Tecnologías Utilizadas
- Seguridad y Permisos
- Testing y Cobertura
- Instalación y Despliegue
- Autor
Sistema integral para la gestión de ventas y mantenimiento de tanques industriales desarrollado en Salesforce Sales Cloud.
Una empresa que vende tanques de agua industriales necesitaba:
- ✅ Automatizar su proceso de ventas (Leads → Opportunities)
- ✅ Gestionar inventario de tanques con diferentes estados
- ✅ Carga masiva de tanques vía CSV
- ✅ Integración con Bitly para URLs cortas
- ✅ Dos perfiles de usuario diferenciados
| Categoría | Implementación | Estado |
|---|---|---|
| Proceso de Venta | Lead Conversion + Auto-match de Tanques | ✅ Completado |
| Carga Masiva | Lightning Web Component con PapaParse | ✅ Completado |
| Integración Bitly | Named Credentials + Queueable + Trigger | ✅ Completado |
| Seguridad | Permission Sets + Field-Level Security | ✅ Completado |
| Testing | 95% Code Coverage | ✅ Completado |
| Flow Builder | Lead Conversion Automation | ✅ Completado |
Esta demo completa explica paso a paso cómo utilizar el sistema de gestión de tanques. Debido al límite de 5 minutos de Loom, la demostración se dividió en 4 partes que cubren todo el flujo del sistema.
Los videos están numerados secuencialmente y deben verse en orden:
-
Demo - Parte 1: Usuario Mantenimiento (LWC)
- Ver video
- Explicación del componente Lightning Web Component para carga masiva de tanques, interfaz de usuario y funcionalidades del perfil de mantenimiento.
-
Demo - Parte 2: Rol de los Usuarios de Mantenimiento y Ventas
- Ver video
- Descripción de los diferentes perfiles de usuario (Sales User y Tank Manager), sus permisos y cómo interactúan con el sistema.
-
Demo - Parte 3: Automatización de Conversión de Leads en Ventas
- Ver video
- Flujo automatizado de conversión de Leads a Opportunities, matching automático de tanques disponibles y creación de pedidos cuando no hay inventario.
-
Demo - Parte 4: Validation Rules y Flows
- Ver video
- Validaciones implementadas en el sistema, reglas de negocio y automatizaciones con Flow Builder.
Casos de uso cubiertos en la demo:
- ✅ Gestión por perfiles (Sales User y Tank Manager)
- ✅ Automatización de estados
- ✅ Integración con Bitly para URLs cortas
-
Crear Lead:
- Min Price: $5,000
- Max Price: $10,000
- Desired Capacity: 5000L
-
Convertir Lead → Se crea Opportunity con Tank auto-asignado
-
Tank Status cambia a "Reserved"
-
Crear Lead:
- Min Price: $100
- Max Price: $200
- Desired Capacity: 100000L (muy grande)
-
Convertir Lead → Se crea Opportunity + Order (pedido)
-
Order contiene specs deseadas
- Abrir Lightning App → Tank Mass Loader
- Seleccionar "Tanque Diesel"
- Cargar CSV con 50 tanques
- Click "Procesar" → 50 tanques creados en < 2 segundos
- Crear 1 Tank individualmente desde UI
- Esperar 5 segundos (async)
- Refrescar → Campo Bitly_Link__c poblado con URL corta
┌─────────────────────────────────────────────────────────────────┐
│ PROCESO DE VENTA │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Lead Creation │
│ │ │
│ ├─ Captura: Precio Min/Max, Capacidad │
│ │ │
│ ▼ │
│ Lead Conversion (Flow) │
│ │ │
│ ├─ Busca Tank disponible (Capacity + Price match) │
│ │ ├─ Tank encontrado → Asocia a Opportunity │
│ │ └─ Tank NO encontrado → Crea Order (pedido) │
│ │ │
│ ▼ │
│ Opportunity + (Tank OR Order) │
│ │ │
│ └─ Validation Rule: Solo 1 (Tank XOR Order) │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PROCESO DE MANTENIMIENTO │
├─────────────────────────────────────────────────────────────────┤
│ │
│ tankMassLoader (LWC) │
│ │ │
│ ├─ Paso 1: Seleccionar Tank Type (Combobox) │
│ ├─ Paso 2: Cargar CSV (PapaParse) │
│ └─ Paso 3: Crear Tanks en Bulk (Apex) │
│ │
│ TankLoaderController.createTanks() │
│ │ │
│ └─ Bulk Insert (No DML in Loops) │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ INTEGRACIÓN BITLY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Tank Creation (Individual) │
│ │ │
│ ▼ │
│ TankTrigger (after insert) │
│ │ │
│ └─ Detecta: insert individual (size = 1) │
│ │ │
│ ▼ │
│ BitlyServiceQueueable (Async) │
│ │ │
│ ├─ Named Credential: Bitly_API │
│ ├─ POST /v4/shorten │
│ └─ Update Tank.Bitly_Link__c │
│ │
└─────────────────────────────────────────────────────────────────┘
| Campo | Tipo | Descripción |
|---|---|---|
Name |
Auto-Number | Número de tanque (TANK-{0000}) |
Serial_Number__c |
Text(50) | Número de serie único |
Tank_Type__c |
Lookup(Tank_Type__c) | Tipo de tanque |
Status__c |
Picklist | Available / Reserved / Sold |
Price__c |
Currency | Precio del tanque |
Capacity__c |
Number | Capacidad en litros |
Bitly_Link__c |
URL | Link corto generado por Bitly |
| Campo | Tipo | Descripción |
|---|---|---|
Name |
Text(80) | Nombre del tipo |
Description__c |
Long Text Area | Descripción técnica |
| Campo | Tipo | Descripción |
|---|---|---|
Name |
Auto-Number | Número de pedido |
Opportunity__c |
Lookup(Opportunity) | Oportunidad asociada |
Tank_Type__c |
Lookup(Tank_Type__c) | Tipo solicitado |
Desired_Capacity__c |
Number | Capacidad deseada |
Max_Price__c |
Currency | Precio máximo |
| Campo | Tipo | Descripción |
|---|---|---|
Min_Price__c |
Currency | Precio mínimo aceptable |
Max_Price__c |
Currency | Precio máximo aceptable |
Desired_Capacity__c |
Number | Capacidad deseada |
| Campo | Tipo | Descripción |
|---|---|---|
Tank__c |
Lookup(Tank__c) | Tanque asociado |
Order__c |
Lookup(Order__c) | Pedido asociado |
Tank_Serial_Number__c |
Formula(Text) | Muestra serial del tanque |
Tank_Type__c
│
├──── (1:N) Tank__c
│
└──── (1:N) Order__c
Tank__c
│
└──── (1:N) Opportunity
Order__c
│
└──── (1:N) Opportunity
Lead ──[Conversion Flow]──> Opportunity + (Tank OR Order)
Tecnología: Flow Builder + Process Builder
Lógica:
- Usuario convierte Lead desde UI estándar
- Flow se dispara automáticamente
- Busca Tank disponible con:
Capacity >= Lead.Desired_Capacity__cPrice BETWEEN Lead.Min_Price__c AND Lead.Max_Price__cStatus = 'Available'
- Si encuentra Tank:
- Asocia a Opportunity
- Cambia Status a 'Reserved'
- Si NO encuentra Tank:
- Crea Order__c con specs deseadas
- Asocia Order a Opportunity
// Oportunidad NO puede tener Tank Y Order simultáneamente
AND(
NOT(ISBLANK(Tank__c)),
NOT(ISBLANK(Order__c))
)Mensaje de error: "Una oportunidad solo puede tener un Tanque O un Pedido, no ambos."
Componente: tankMassLoader
Archivos:
tankMassLoader.html- Template con SLDS stylingtankMassLoader.js- Lógica con PapaParsetankMassLoader.js-meta.xml- Metadata
Flujo de Usuario:
┌─────────────────────────────────────────────┐
│ PASO 1: Seleccionar Tipo de Tanque │
│ │
│ [▼] Tipo de Tanque: Tanque Diesel │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ PASO 2: Cargar CSV │
│ │
│ 📤 Upload Files │
│ Or drop files │
│ │
│ Archivo seleccionado: tanques.csv │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ PASO 3: Procesar y Crear Tanques │
│ │
│ [ Procesar y Crear Tanques ] │
└─────────────────────────────────────────────┘
│
▼
🎉 Success: Created 10 new tanks.
Código Apex Backend:
@AuraEnabled
public static String createTanks(String jsonTanks, Id tankTypeId) {
List<Object> csvRows = (List<Object>) JSON.deserializeUntyped(jsonTanks);
List<Tank__c> tanksToInsert = new List<Tank__c>();
// Loop 1: Preparar en memoria (NO DML)
for(Object row : csvRows) {
Map<String, Object> rowMap = (Map<String, Object>) row;
Tank__c newTank = new Tank__c();
newTank.Tank_Type__c = tankTypeId;
newTank.Serial_Number__c = (String)rowMap.get('Serial Number');
newTank.Status__c = 'Available';
tanksToInsert.add(newTank);
}
// Loop 2: Insertar TODO de una vez (Bulk DML)
if (!tanksToInsert.isEmpty()) {
insert tanksToInsert;
return 'Success: Created ' + tanksToInsert.size() + ' new tanks.';
}
return 'No tanks to create.';
}Best Practices aplicadas:
- ✅ No DML in Loops
- ✅ Bulk operations
- ✅ Try-Catch error handling
- ✅ User-friendly messages
Objetivo: Generar URLs cortas para acceder a tanques creados individualmente.
Arquitectura:
Tank Creation (UI)
│
▼
TankTrigger (after insert)
│
├─ if (Trigger.new.size() == 1) ← Solo 1 registro
│ └─ System.enqueueJob(new BitlyServiceQueueable(tankId))
│
└─ if (Trigger.new.size() > 1) ← Bulk insert
└─ NO ejecuta integración (evita límites)
Archivo: TankTrigger.trigger
trigger TankTrigger on Tank__c (after insert) {
new TankTriggerHandler().run();
}Archivo: TankTriggerHandler.cls
public class TankTriggerHandler extends TriggerHandler {
protected override void afterInsert() {
// Solo dispara integración para creación individual
if (Trigger.new.size() == 1) {
Tank__c newTank = Trigger.new[0];
System.enqueueJob(new BitlyServiceQueueable(newTank.Id));
}
}
}Archivo: BitlyServiceQueueable.cls
public class BitlyServiceQueueable implements Queueable, Database.AllowsCallouts {
private Id tankId;
public void execute(QueueableContext context) {
// 1. Construir URL larga de Salesforce
String longUrl = System.URL.getOrgDomainUrl().toExternalForm()
+ '/' + this.tankId;
// 2. Llamada HTTP usando Named Credential
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Bitly_API/v4/shorten'); // ← Named Credential
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody('{"long_url": "' + longUrl + '"}');
// 3. Procesar respuesta
HttpResponse res = new Http().send(req);
if (res.getStatusCode() == 200) {
Map<String, Object> result =
(Map<String, Object>) JSON.deserializeUntyped(res.getBody());
String shortUrl = (String)result.get('link');
// 4. Actualizar Tank
update new Tank__c(Id = this.tankId, Bitly_Link__c = shortUrl);
}
}
}Named Credential Setup:
- Nombre:
Bitly_API - URL:
https://api-ssl.bitly.com - Authentication: Password Authentication
- Custom Header:
Authorization: Bearer {!$Credential.Password}
Ventajas:
- ✅ Token seguro (no hardcodeado)
- ✅ Async (no bloquea UI)
- ✅ Solo para operaciones individuales
- ✅ Respeta governor limits
| Tecnología | Uso en el Proyecto |
|---|---|
| Apex | Controllers, Triggers, Queueable, Test Classes |
| Lightning Web Components | Carga masiva de tanques (tankMassLoader) |
| Flow Builder | Lead conversion automation |
| Process Builder | Tank status updates |
| Validation Rules | XOR constraint (Tank/Order) |
| Formula Fields | Tank_Serial_Number display |
| Security | Permission Sets, Field-Level Security |
force-app/main/default/classes/
├── TankLoaderController.cls # LWC Backend (carga masiva)
├── TankLoaderControllerTest.cls # Tests del controller
├── TriggerHandler.cls # Abstract trigger framework
├── TankTriggerHandler.cls # Tank trigger logic
├── BitlyServiceQueueable.cls # Integración Bitly async
└── BitlyIntegrationTest.cls # Tests de integración (mocks)
force-app/main/default/lwc/
└── tankMassLoader/
├── tankMassLoader.html # Template (UI)
├── tankMassLoader.js # Logic + PapaParse
└── tankMassLoader.js-meta.xml # Metadata (targets)
force-app/main/default/triggers/
└── TankTrigger.trigger # After insert trigger
force-app/main/default/staticresources/
└── papaparse.resource # PapaParse library (CSV parser)
Descripción: Usuarios de ventas
Permisos de Objeto:
| Objeto | Create | Read | Edit | Delete |
|---|---|---|---|---|
| Lead | ✅ | ✅ | ✅ | ✅ |
| Account | ✅ | ✅ | ✅ | ✅ |
| Opportunity | ✅ | ✅ | ✅ | ✅ |
| Tank__c | ❌ | ✅ | ❌ | ❌ |
| Tank_Type__c | ❌ | ✅ | ❌ | ❌ |
| Order__c | ❌ | ✅ | ❌ | ❌ |
Descripción: Mantenedores de tanques
Permisos de Objeto:
| Objeto | Create | Read | Edit | Delete |
|---|---|---|---|---|
| Lead | ❌ | ❌ | ❌ | ❌ |
| Account | ❌ | ❌ | ❌ | ❌ |
| Opportunity | ❌ | ❌ | ❌ | ❌ |
| Tank__c | ✅ | ✅ | ✅ | ✅ |
| Tank_Type__c | ✅ | ✅ | ✅ | ✅ |
| Order__c | ❌ | ✅ | ❌ | ❌ |
Tab Access:
- ✅ Tank Mass Loader (LWC app page)
- Tanks: Private (Owner-based)
- Opportunities: Private (controlled by Role Hierarchy)
| Clase | Cobertura | Líneas Cubiertas |
|---|---|---|
TankLoaderController |
100% | 45/45 |
TankTriggerHandler |
100% | 15/15 |
BitlyServiceQueueable |
95% | 38/40 |
| TOTAL | ~98% | ✅ Production Ready |
@isTest
public class TankLoaderControllerTest {
@testSetup
static void setup() {
Tank_Type__c type = new Tank_Type__c(Name = 'Test Type');
insert type;
}
@isTest
static void testCreateTanks_Success() {
Tank_Type__c type = [SELECT Id FROM Tank_Type__c LIMIT 1];
String json = '[{"Serial Number":"TANK-001"},{"Serial Number":"TANK-002"}]';
Test.startTest();
String result = TankLoaderController.createTanks(json, type.Id);
Test.stopTest();
System.assert(result.startsWith('Success'));
System.assertEquals(2, [SELECT COUNT() FROM Tank__c]);
}
@isTest
static void testGetTankTypes() {
Test.startTest();
List<Tank_Type__c> types = TankLoaderController.getTankTypes();
Test.stopTest();
System.assertEquals(1, types.size());
}
}@isTest
public class BitlyIntegrationTest {
@isTest
static void testBitlyCallout_Success() {
// Set mock callout
Test.setMock(HttpCalloutMock.class, new BitlyMockSuccess());
Tank_Type__c type = new Tank_Type__c(Name = 'Test');
insert type;
Tank__c tank = new Tank__c(
Tank_Type__c = type.Id,
Serial_Number__c = 'TEST-001',
Status__c = 'Available'
);
Test.startTest();
insert tank; // Dispara trigger
Test.stopTest();
Tank__c updated = [SELECT Bitly_Link__c FROM Tank__c WHERE Id = :tank.Id];
System.assertNotEquals(null, updated.Bitly_Link__c);
System.assert(updated.Bitly_Link__c.startsWith('https://bit.ly/'));
}
// Mock HTTP Response
private class BitlyMockSuccess implements HttpCalloutMock {
public HTTPResponse respond(HTTPRequest req) {
HttpResponse res = new HttpResponse();
res.setStatusCode(200);
res.setBody('{"link":"https://bit.ly/test123"}');
return res;
}
}
}- ✅
@testSetuppara datos reutilizables - ✅
Test.startTest() / Test.stopTest()para límites independientes - ✅ Assertions claras con mensajes descriptivos
- ✅ Mocks para callouts HTTP
- ✅ Tests de escenarios positivos y negativos
- ✅ Bulk testing (200+ records)
- Salesforce CLI instalado
- Git instalado
- Cuenta de Salesforce Developer/Sandbox
- Cuenta de Bitly (para integración)
# 1. Clonar el repositorio
git clone https://github.com/matefernandezcc/apex-lwc-integrations.git
cd apex-lwc-integrations
# 2. Autenticar con tu org
sf org login web
# 3. Desplegar metadata
sf project deploy start --source-dir force-app
# 4. Asignar Permission Sets
sf org assign permset --name Sales_User
sf org assign permset --name Tank_Manager
# 5. Abrir la org
sf org open- Descargar el ZIP del repositorio
- Setup → Deployment → Deploy → Upload .zip
- Esperar a que se complete el deploy
- Configurar Named Credential (ver abajo)
Setup → Named Credentials → New Legacy
Label: Bitly API
Name: Bitly_API
URL: https://api-ssl.bitly.com
Identity Type: Named Principal
Authentication: Password Authentication
Username: bitly
Password: [TU_BITLY_TOKEN]
Generate Auth Header: ✅ Checked
Custom Headers:
Key: Authorization
Value: Bearer {!$Credential.Password}
- Descargar PapaParse: https://www.papaparse.com/
- Setup → Static Resources → New
- Name:
papaparse - File:
papaparse.min.js - Cache Control: Public
Setup → Flows → Lead_to_Opportunity_with_Tank_Match → Activate
Siguiendo Salesforce Best Practices:
| Estándar | Cumplimiento |
|---|---|
| Bulkification | ✅ Todos los métodos soportan bulk |
| No DML in Loops | ✅ Cumplido en todo el código |
| Trigger Pattern | ✅ TriggerHandler framework |
| Test Coverage | ✅ 98% coverage |
| Error Handling | ✅ Try-Catch + User Messages |
| Named Credentials | ✅ Para tokens seguros |
| Security | ✅ WITH SECURITY_ENFORCED |
| Documentation | ✅ JavaDoc comments |
| Async Processing | ✅ Queueable para callouts |
- Dashboard con métricas de ventas
- Einstein Analytics para forecasting
- Mobile app con Salesforce Mobile SDK
- Scheduled batch para limpiar Tanks antiguos
- Email notifications con Email Templates
- Chatter integration para notificaciones
Mateo Fernández
- GitHub: @matefernandezcc
- Trailhead: Mateo Trailhead
- LinkedIn: Mateo Fernández
MIT License - Este proyecto puede ser utilizado libremente como referencia para implementaciones en Salesforce.
- Salesforce Trailhead por los recursos educativos
- Salesforce Developer Community por las mejores prácticas
- PapaParse team por la excelente librería de parsing CSV