feat: Primeira Versão traduzida
25
.dockerignore
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
14
CarneiroTech.csproj
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Markdig" Version="0.44.0" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
25
CarneiroTech.sln
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 18
|
||||||
|
VisualStudioVersion = 18.1.11304.174 d18.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CarneiroTech", "CarneiroTech.csproj", "{BFC5463B-C1CF-69EE-98AF-C5790D1C99C2}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{BFC5463B-C1CF-69EE-98AF-C5790D1C99C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BFC5463B-C1CF-69EE-98AF-C5790D1C99C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BFC5463B-C1CF-69EE-98AF-C5790D1C99C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BFC5463B-C1CF-69EE-98AF-C5790D1C99C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {9322B71E-A202-497C-8EB5-48B61812278D}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
329
Content/Cases/en/asp-to-dotnet-migration.md
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
---
|
||||||
|
title: "ASP 3.0 to .NET Core Migration - Cargo Tracking System"
|
||||||
|
slug: "asp-to-dotnet-migration"
|
||||||
|
summary: "Tech Lead in gradual migration of mission-critical ASP 3.0 system to .NET Core, with dual-write data synchronization and cost reduction of $20k/year in mapping APIs."
|
||||||
|
client: "Logistics and Tracking Company"
|
||||||
|
industry: "Logistics & Security"
|
||||||
|
timeline: "12 months (complete migration)"
|
||||||
|
role: "Tech Lead & Solution Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- ASP Classic
|
||||||
|
- .NET Core
|
||||||
|
- SQL Server
|
||||||
|
- Migration
|
||||||
|
- Tech Lead
|
||||||
|
- OSRM
|
||||||
|
- APIs
|
||||||
|
- Architecture
|
||||||
|
featured: true
|
||||||
|
order: 2
|
||||||
|
date: 2015-06-01
|
||||||
|
seo_title: "ASP 3.0 to .NET Core Migration - Carneiro Tech Case Study"
|
||||||
|
seo_description: "Case study of gradual ASP 3.0 to .NET Core migration with data synchronization and $20k/year cost savings in API expenses."
|
||||||
|
seo_keywords: "ASP migration, .NET Core, legacy modernization, SQL Server, OSRM, tech lead, routing API"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Mission-critical cargo monitoring system for high-value loads (LED TVs worth $600 each, shipments up to 1000 units) using GPS satellite tracking. The application covered the entire lifecycle: from driver registration and evaluation (police background checks) to real-time monitoring and final delivery.
|
||||||
|
|
||||||
|
**Main challenge:** Migrate legacy ASP 3.0 application to .NET Core with zero downtime, maintaining 24/7 critical operations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Critical Legacy System
|
||||||
|
|
||||||
|
The company operated a mission-critical system in **ASP 3.0** (Classic ASP) that couldn't stop:
|
||||||
|
|
||||||
|
**Legacy technology:**
|
||||||
|
- ASP 3.0 (1998 technology)
|
||||||
|
- SQL Server 2005
|
||||||
|
- On-premises failover cluster (perfectly capable of handling the load)
|
||||||
|
- Integration with GPS satellite trackers
|
||||||
|
- Google Maps API (cost: **$20,000/year** just for route calculation)
|
||||||
|
|
||||||
|
**Constraints:**
|
||||||
|
- 24/7 system operation with high-value cargo
|
||||||
|
- No downtime allowed during migration
|
||||||
|
- Multiple interdependent modules
|
||||||
|
- Team needed to continue developing features during migration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### Phase 1: Infrastructure Preparation (Months 1-3)
|
||||||
|
|
||||||
|
#### Database Upgrade
|
||||||
|
```
|
||||||
|
SQL Server 2005 → SQL Server 2014
|
||||||
|
- Full backup and validation
|
||||||
|
- Stored procedure migration
|
||||||
|
- Index optimization
|
||||||
|
- Performance testing
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dual-Write Synchronization Strategy
|
||||||
|
|
||||||
|
I implemented a **bidirectional synchronization system** that allowed:
|
||||||
|
|
||||||
|
1. **New modules (.NET Core)** wrote to the new database
|
||||||
|
2. **Automatic trigger** synchronized data to the legacy database
|
||||||
|
3. **Old modules (ASP 3.0)** continued working normally
|
||||||
|
4. **Zero downtime** throughout the entire migration
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Synchronization implementation example
|
||||||
|
public class DualWriteService
|
||||||
|
{
|
||||||
|
public async Task SaveDriver(Driver driver)
|
||||||
|
{
|
||||||
|
// Write to new database (.NET Core)
|
||||||
|
await _newDbContext.Drivers.AddAsync(driver);
|
||||||
|
await _newDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
// SQL trigger automatically syncs to legacy database
|
||||||
|
// ASP 3.0 modules continue functioning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this approach?**
|
||||||
|
- Enabled **module-by-module** migration
|
||||||
|
- Team could continue developing
|
||||||
|
- Simple rollback if needed
|
||||||
|
- Reduced operational risk
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Gradual Module Migration (Months 4-12)
|
||||||
|
|
||||||
|
I migrated modules in increasing complexity order:
|
||||||
|
|
||||||
|
**Migration sequence:**
|
||||||
|
1. ✅ Basic registrations (drivers, vehicles)
|
||||||
|
2. ✅ Risk assessment (police database integration)
|
||||||
|
3. ✅ Cargo and route management
|
||||||
|
4. ✅ Real-time GPS monitoring
|
||||||
|
5. ✅ Alerts and notifications
|
||||||
|
6. ✅ Reports and analytics
|
||||||
|
|
||||||
|
**Migrated application stack:**
|
||||||
|
- `.NET Core 1.0` (2015-2016 was the beginning of .NET Core)
|
||||||
|
- `Entity Framework Core`
|
||||||
|
- `SignalR` for real-time monitoring
|
||||||
|
- `SQL Server 2014`
|
||||||
|
- RESTful APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Cost Reduction with OSRM ($20k/year Savings)
|
||||||
|
|
||||||
|
#### Problem: Prohibitive Google Maps Cost
|
||||||
|
|
||||||
|
The company spent **$20,000/year** just on Google Maps Directions API for truck route calculation.
|
||||||
|
|
||||||
|
#### Solution: OSRM (Open Source Routing Machine)
|
||||||
|
|
||||||
|
I implemented a solution based on **OSRM** (open-source routing engine):
|
||||||
|
|
||||||
|
**Solution architecture:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Frontend │
|
||||||
|
│ (Leaflet.js) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ┌──────────────┐
|
||||||
|
│ API Wrapper │─────▶│ OSRM Server │
|
||||||
|
│ (.NET Core) │ │ (self-hosted)│
|
||||||
|
└────────┬────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Google Maps │
|
||||||
|
│ (display only) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
|
||||||
|
1. **OSRM Server configured** on own server
|
||||||
|
2. **User-friendly API wrapper** in .NET Core that:
|
||||||
|
- Received origin/destination
|
||||||
|
- Queried OSRM (free)
|
||||||
|
- Returned all route points
|
||||||
|
- Formatted for frontend
|
||||||
|
3. **Frontend** drew the route on Google Maps (visualization only, no routing API)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("route")]
|
||||||
|
public async Task<IActionResult> GetRoute(double originLat, double originLng,
|
||||||
|
double destLat, double destLng)
|
||||||
|
{
|
||||||
|
// Query OSRM (free)
|
||||||
|
var osrmResponse = await _osrmClient.GetRouteAsync(
|
||||||
|
originLat, originLng, destLat, destLng);
|
||||||
|
|
||||||
|
// Return formatted points for frontend
|
||||||
|
return Ok(new {
|
||||||
|
points = osrmResponse.Routes[0].Geometry.Coordinates,
|
||||||
|
distance = osrmResponse.Routes[0].Distance,
|
||||||
|
duration = osrmResponse.Routes[0].Duration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend with Leaflet:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Draw route on map (Google Maps only for tiles)
|
||||||
|
L.polyline(routePoints, {color: 'red'}).addTo(map);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### OpenStreetMap Attempt
|
||||||
|
|
||||||
|
I tried to also replace Google Maps (tiles) with **OpenStreetMap**, which worked technically, but:
|
||||||
|
|
||||||
|
❌ **Users didn't like** the appearance
|
||||||
|
❌ Preferred the familiar Google Maps interface
|
||||||
|
|
||||||
|
✅ **Decision:** Keep Google Maps for visualization only (no routing API cost)
|
||||||
|
|
||||||
|
**Result:** Savings of **~$20,000/year** while maintaining route quality.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Complete Migration in 12 Months
|
||||||
|
|
||||||
|
✅ **100% of modules** migrated from ASP 3.0 to .NET Core
|
||||||
|
✅ **Zero downtime** throughout the entire migration
|
||||||
|
✅ **Productive team** throughout the process
|
||||||
|
✅ Faster and more scalable system
|
||||||
|
|
||||||
|
### Cost Reduction
|
||||||
|
|
||||||
|
💰 **$20,000/year saved** by replacing Google Maps Directions API
|
||||||
|
📉 **Optimized infrastructure** with SQL Server 2014
|
||||||
|
|
||||||
|
### Technical Improvements
|
||||||
|
|
||||||
|
🚀 **Performance:** .NET Core application 3x faster than ASP 3.0
|
||||||
|
🔒 **Security:** Modern stack with active security patches
|
||||||
|
🛠️ **Maintainability:** Modern C# code vs legacy VBScript
|
||||||
|
📊 **Monitoring:** SignalR for more efficient real-time tracking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unexecuted Phase: Microservices & Cloud
|
||||||
|
|
||||||
|
### Initial Planning
|
||||||
|
|
||||||
|
I participated in the **design and conception** of the second phase (never executed):
|
||||||
|
|
||||||
|
**Planned architecture:**
|
||||||
|
- Migration to **Azure** (cloud was just starting in 2015)
|
||||||
|
- Break into **microservices**:
|
||||||
|
- Authentication service
|
||||||
|
- GPS/tracking service
|
||||||
|
- Routing service
|
||||||
|
- Notification service
|
||||||
|
- **Event-driven architecture** with message queues
|
||||||
|
|
||||||
|
**Why it wasn't executed:**
|
||||||
|
|
||||||
|
I left the company right after completing the .NET Core migration. The second phase was planned but not implemented by me.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`ASP 3.0` `VBScript` `.NET Core 1.0` `C#` `Entity Framework Core` `SQL Server 2005` `SQL Server 2014` `OSRM` `Leaflet.js` `Google Maps` `SignalR` `REST APIs` `GPS/Satellite` `Migration Strategy` `Dual-Write Pattern`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Why dual-write synchronization?
|
||||||
|
|
||||||
|
**Alternatives considered:**
|
||||||
|
1. ❌ Big Bang migration (too risky)
|
||||||
|
2. ❌ Keep everything in ASP 3.0 (unsustainable)
|
||||||
|
3. ✅ **Gradual migration with sync** (chosen)
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Critical system couldn't stop
|
||||||
|
- Enabled module-by-module rollback
|
||||||
|
- Team remained productive
|
||||||
|
|
||||||
|
### Why OSRM instead of others?
|
||||||
|
|
||||||
|
**Alternatives:**
|
||||||
|
- Google Maps: $20k/year ❌
|
||||||
|
- Mapbox: Paid license ❌
|
||||||
|
- GraphHopper: Complex setup ❌
|
||||||
|
- **OSRM: Open-source, fast, configurable** ✅
|
||||||
|
|
||||||
|
### Why not OpenStreetMap for tiles?
|
||||||
|
|
||||||
|
**UX-based decision:**
|
||||||
|
- Technically worked perfectly
|
||||||
|
- Users preferred familiar Google interface
|
||||||
|
- **Compromise:** Google Maps for visualization (free) + OSRM for routing (free)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Gradual Migration > Big Bang
|
||||||
|
|
||||||
|
Migrating module by module with synchronization enabled:
|
||||||
|
- Continuous learning
|
||||||
|
- Route adjustments during the process
|
||||||
|
- Team and stakeholder confidence
|
||||||
|
|
||||||
|
### 2. Open Source Can Save a Lot
|
||||||
|
|
||||||
|
OSRM saved **$20k/year** without quality loss. But requires:
|
||||||
|
- Expertise to configure
|
||||||
|
- Own infrastructure
|
||||||
|
- Continuous maintenance
|
||||||
|
|
||||||
|
### 3. UX > Technology Sometimes
|
||||||
|
|
||||||
|
OpenStreetMap was technically superior (free), but users preferred Google Maps. **Lesson:** Listen to end users.
|
||||||
|
|
||||||
|
### 4. Plan for Cloud, but Validate ROI
|
||||||
|
|
||||||
|
In 2015, cloud was just starting. On-premises infrastructure (SQL Server cluster) was perfectly capable. **Don't force cloud if there's no clear benefit.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context: Why 2015 Was a Special Moment?
|
||||||
|
|
||||||
|
**State of technology in 2015:**
|
||||||
|
|
||||||
|
- ☁️ **Cloud in early stages:** AWS existed, Azure growing, but low corporate adoption
|
||||||
|
- 🆕 **.NET Core 1.0 launched** in June 2016 (we used RC during the project)
|
||||||
|
- 📱 **Microservices:** New concept, Docker in early adoption
|
||||||
|
- 🗺️ **Google Maps dominant:** Paid APIs, few mature open-source alternatives
|
||||||
|
|
||||||
|
**Challenges of the time:**
|
||||||
|
- Non-existent ASP→.NET migration tools
|
||||||
|
- Scarce .NET Core documentation (version 1.0!)
|
||||||
|
- Architecture patterns still consolidating
|
||||||
|
|
||||||
|
This project was **pioneering** in adopting .NET Core right at the beginning, when most were migrating to .NET Framework 4.x.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Result:** Successful migration of 24/7 critical system, $20k/year savings, and solid foundation for future evolution.
|
||||||
|
|
||||||
|
[Want to discuss a similar migration? Get in touch](#contact)
|
||||||
382
Content/Cases/en/cnpj-fast-process.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
---
|
||||||
|
title: "CNPJ Fast - Alphanumeric CNPJ Migration Process"
|
||||||
|
slug: "cnpj-fast-process"
|
||||||
|
summary: "Creation of structured methodology for migrating applications to the new Brazilian alphanumeric CNPJ format, sold to insurance company and collection agency."
|
||||||
|
client: "Consulting Firm (Internal)"
|
||||||
|
industry: "Consulting & Digital Transformation"
|
||||||
|
timeline: "3 months (process creation)"
|
||||||
|
role: "Solution Architect & Process Designer"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- Process Design
|
||||||
|
- CNPJ
|
||||||
|
- Migration Strategy
|
||||||
|
- Regulatory Compliance
|
||||||
|
- Consulting
|
||||||
|
- Sales Enablement
|
||||||
|
featured: true
|
||||||
|
order: 3
|
||||||
|
date: 2024-09-01
|
||||||
|
seo_title: "CNPJ Fast - Alphanumeric CNPJ Migration Methodology"
|
||||||
|
seo_description: "Case study of creating a structured process for migrating to Brazilian alphanumeric CNPJ, sold to insurance company and collection agency."
|
||||||
|
seo_keywords: "CNPJ alphanumeric, migration process, regulatory compliance, consulting, methodology"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
With the introduction of **alphanumeric CNPJ** by the Brazilian Federal Revenue Service, companies faced the challenge of adapting their legacy applications that stored CNPJ as numeric fields (`bigint`, `numeric`, `int`).
|
||||||
|
|
||||||
|
I created **CNPJ Fast**, a structured methodology to assess, plan, and execute CNPJ migrations in corporate applications and databases.
|
||||||
|
|
||||||
|
**Result:** Process sold to **2 clients** (insurance company and collection agency) before implementation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Complex Regulatory Change
|
||||||
|
|
||||||
|
**Regulatory context:**
|
||||||
|
- Brazilian Federal Revenue Service introduced **alphanumeric CNPJ**
|
||||||
|
- CNPJ is no longer just numbers (14 digits)
|
||||||
|
- Now accepts **letters and numbers** (alphanumeric format)
|
||||||
|
|
||||||
|
**Impact on companies:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- BEFORE: Numeric CNPJ
|
||||||
|
CNPJ BIGINT -- 12345678000190
|
||||||
|
|
||||||
|
-- AFTER: Alphanumeric CNPJ
|
||||||
|
CNPJ VARCHAR(18) -- 12.ABC.678/0001-90
|
||||||
|
```
|
||||||
|
|
||||||
|
**Identified problems:**
|
||||||
|
|
||||||
|
1. **Database:** `BIGINT`, `NUMERIC`, `INT` columns don't support characters
|
||||||
|
2. **Primary keys:** CNPJ used as PK in several tables
|
||||||
|
3. **Foreign keys:** Relationships between tables
|
||||||
|
4. **Volume:** Millions of records to migrate
|
||||||
|
5. **Applications:** Validations, masks, business rules
|
||||||
|
6. **Testing:** Ensure integrity after migration
|
||||||
|
7. **Downtime:** Limited maintenance windows
|
||||||
|
|
||||||
|
**Without a structured process**, companies risked:
|
||||||
|
- Data loss
|
||||||
|
- Database inconsistencies
|
||||||
|
- Broken applications
|
||||||
|
- Extended downtime
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution: CNPJ Fast Process
|
||||||
|
|
||||||
|
### 5-Phase Methodology
|
||||||
|
|
||||||
|
I designed a structured process that could be replicated across different clients:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ PHASE 1: DISCOVERY & ASSESSMENT │
|
||||||
|
│ - Application inventory │
|
||||||
|
│ - Database schema analysis │
|
||||||
|
│ - Identification of impacted tables │
|
||||||
|
│ - Data volume estimation │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ PHASE 2: IMPACT ANALYSIS │
|
||||||
|
│ - Dependency mapping │
|
||||||
|
│ - Analysis of primary/foreign keys │
|
||||||
|
│ - Identification of business rules │
|
||||||
|
│ - Risk assessment │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ PHASE 3: MIGRATION PLANNING │
|
||||||
|
│ - Migration strategy (phased commits) │
|
||||||
|
│ - Automated SQL scripts │
|
||||||
|
│ - Rollback plan │
|
||||||
|
│ - Maintenance windows │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ PHASE 4: EXECUTION │
|
||||||
|
│ - Batch data migration │
|
||||||
|
│ - Application updates │
|
||||||
|
│ - Integration testing │
|
||||||
|
│ - Integrity validation │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ PHASE 5: VALIDATION & GO-LIVE │
|
||||||
|
│ - Regression testing │
|
||||||
|
│ - Performance validation │
|
||||||
|
│ - Coordinated go-live │
|
||||||
|
│ - Post-migration monitoring │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1: Discovery & Assessment
|
||||||
|
|
||||||
|
**Objective:** Understand the complete migration scope
|
||||||
|
|
||||||
|
**Deliverables:**
|
||||||
|
|
||||||
|
1. **Application Inventory**
|
||||||
|
- List of applications using CNPJ
|
||||||
|
- Technologies (ASP 3.0, VB6, .NET, microservices)
|
||||||
|
- Criticality of each application
|
||||||
|
|
||||||
|
2. **Schema Analysis**
|
||||||
|
```sql
|
||||||
|
-- Automated discovery script
|
||||||
|
SELECT
|
||||||
|
t.TABLE_SCHEMA,
|
||||||
|
t.TABLE_NAME,
|
||||||
|
c.COLUMN_NAME,
|
||||||
|
c.DATA_TYPE,
|
||||||
|
c.CHARACTER_MAXIMUM_LENGTH
|
||||||
|
FROM INFORMATION_SCHEMA.TABLES t
|
||||||
|
JOIN INFORMATION_SCHEMA.COLUMNS c
|
||||||
|
ON t.TABLE_NAME = c.TABLE_NAME
|
||||||
|
WHERE c.COLUMN_NAME LIKE '%CNPJ%'
|
||||||
|
AND c.DATA_TYPE IN ('bigint', 'numeric', 'int')
|
||||||
|
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Volume Estimation**
|
||||||
|
- Total records per table
|
||||||
|
- Size in GB
|
||||||
|
- Estimated migration time
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
|
||||||
|
| Table | Column | Current Type | Records | Criticality |
|
||||||
|
|--------|--------|------------|-----------|-------------|
|
||||||
|
| Clients | CNPJ_Client | BIGINT | 8,000,000 | High |
|
||||||
|
| Suppliers | CNPJ_Supplier | NUMERIC(14) | 2,500,000 | Medium |
|
||||||
|
| Transactions | CNPJ_Payer | BIGINT | 90,000,000 | Critical |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Impact Analysis
|
||||||
|
|
||||||
|
**Objective:** Map all dependencies and risks
|
||||||
|
|
||||||
|
**Key analysis:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Identifies PKs and FKs involving CNPJ
|
||||||
|
SELECT
|
||||||
|
fk.name AS FK_Name,
|
||||||
|
tp.name AS Parent_Table,
|
||||||
|
cp.name AS Parent_Column,
|
||||||
|
tr.name AS Referenced_Table,
|
||||||
|
cr.name AS Referenced_Column
|
||||||
|
FROM sys.foreign_keys fk
|
||||||
|
INNER JOIN sys.tables tp ON fk.parent_object_id = tp.object_id
|
||||||
|
INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
||||||
|
INNER JOIN sys.columns cp ON fkc.parent_column_id = cp.column_id
|
||||||
|
AND fkc.parent_object_id = cp.object_id
|
||||||
|
INNER JOIN sys.tables tr ON fk.referenced_object_id = tr.object_id
|
||||||
|
INNER JOIN sys.columns cr ON fkc.referenced_column_id = cr.column_id
|
||||||
|
AND fkc.referenced_object_id = cr.object_id
|
||||||
|
WHERE cp.name LIKE '%CNPJ%' OR cr.name LIKE '%CNPJ%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risk Assessment:**
|
||||||
|
|
||||||
|
- **High:** Tables with CNPJ as PK and >10M records
|
||||||
|
- **Medium:** Tables with FK to CNPJ
|
||||||
|
- **Low:** Tables without constraints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Migration Planning
|
||||||
|
|
||||||
|
**Gradual migration strategy:**
|
||||||
|
|
||||||
|
To avoid database locks, I designed a **phased commits** strategy:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Strategy for large tables (>1M records)
|
||||||
|
|
||||||
|
-- 1. Add new VARCHAR column
|
||||||
|
ALTER TABLE Clients
|
||||||
|
ADD CNPJ_Client_New VARCHAR(18) NULL;
|
||||||
|
|
||||||
|
-- 2. Migration in batches (phased commits)
|
||||||
|
DECLARE @BatchSize INT = 100000;
|
||||||
|
DECLARE @RowsAffected INT = 1;
|
||||||
|
|
||||||
|
WHILE @RowsAffected > 0
|
||||||
|
BEGIN
|
||||||
|
UPDATE TOP (@BatchSize) Clients
|
||||||
|
SET CNPJ_Client_New = FORMAT(CNPJ_Client, '00000000000000')
|
||||||
|
WHERE CNPJ_Client_New IS NULL;
|
||||||
|
|
||||||
|
SET @RowsAffected = @@ROWCOUNT;
|
||||||
|
WAITFOR DELAY '00:00:01'; -- Pause between batches
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 3. Remove constraints (PKs, FKs)
|
||||||
|
ALTER TABLE Clients DROP CONSTRAINT PK_Clients;
|
||||||
|
|
||||||
|
-- 4. Rename columns
|
||||||
|
EXEC sp_rename 'Clients.CNPJ_Client', 'CNPJ_Client_Old', 'COLUMN';
|
||||||
|
EXEC sp_rename 'Clients.CNPJ_Client_New', 'CNPJ_Client', 'COLUMN';
|
||||||
|
|
||||||
|
-- 5. Recreate constraints
|
||||||
|
ALTER TABLE Clients
|
||||||
|
ADD CONSTRAINT PK_Clients PRIMARY KEY (CNPJ_Client);
|
||||||
|
|
||||||
|
-- 6. Remove old column (after validation)
|
||||||
|
ALTER TABLE Clients DROP COLUMN CNPJ_Client_Old;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this approach?**
|
||||||
|
|
||||||
|
- Avoids locking entire table
|
||||||
|
- Allows pausing/resuming migration
|
||||||
|
- Minimizes production impact
|
||||||
|
- Facilitates rollback if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phases 4 & 5: Execution and Validation
|
||||||
|
|
||||||
|
**Execution checklist:**
|
||||||
|
|
||||||
|
- [ ] Complete database backup
|
||||||
|
- [ ] Execute migration scripts in batches
|
||||||
|
- [ ] Update applications (validations, masks)
|
||||||
|
- [ ] Integration testing
|
||||||
|
- [ ] Referential integrity validation
|
||||||
|
- [ ] Performance testing
|
||||||
|
- [ ] Coordinated go-live
|
||||||
|
- [ ] 24h post-migration monitoring
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sales Enablement: UX Presentation
|
||||||
|
|
||||||
|
**Collaboration with UX Manager:**
|
||||||
|
|
||||||
|
The company's UX manager created an **impactful visual presentation** of the CNPJ Fast process:
|
||||||
|
|
||||||
|
**Presentation content:**
|
||||||
|
- Infographics of the 5-phase process
|
||||||
|
- Examples of time/cost estimates
|
||||||
|
- Use cases (insurance, banks, fintechs)
|
||||||
|
- Executive checklist
|
||||||
|
- Documentation templates
|
||||||
|
|
||||||
|
**Result:** Presentation used by sales team for prospecting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Sales Achieved
|
||||||
|
|
||||||
|
**Client 1: Insurance Company**
|
||||||
|
- Stack: ASP 3.0, VB6 components, .NET, microservices
|
||||||
|
- Scope: Complete legacy application migration
|
||||||
|
- Status: **Project sold** (execution by another team)
|
||||||
|
- Value: [Confidential]
|
||||||
|
|
||||||
|
**Client 2: Collection Agency**
|
||||||
|
- Scope: Database migration (~100M records)
|
||||||
|
- Status: **Project sold and in execution** (by me)
|
||||||
|
- Particularity: Process was **restructured** to meet specific needs
|
||||||
|
- See complete case study: [CNPJ Migration - 100M Records](/cases/cnpj-migration-database)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Business Impact
|
||||||
|
|
||||||
|
**2 projects sold** before first execution
|
||||||
|
**Replicable process** for new clients
|
||||||
|
**Positioning** as specialist in regulatory migrations
|
||||||
|
**Knowledge base** for future similar projects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Technical Impact
|
||||||
|
|
||||||
|
**Tested methodology** in real scenarios
|
||||||
|
**Reusable documentation** (scripts, checklists, templates)
|
||||||
|
**Acceleration** of similar projects (from weeks to days)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`SQL Server` `Migration Strategy` `Process Design` `Regulatory Compliance` `ASP 3.0` `VB6` `.NET` `Microservices` `Batch Processing` `Database Optimization`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Why structured process?
|
||||||
|
|
||||||
|
**Alternatives:**
|
||||||
|
1. Ad-hoc approach per project
|
||||||
|
2. Manual consulting without methodology
|
||||||
|
3. **Replicable and scalable process**
|
||||||
|
|
||||||
|
**Justification:**
|
||||||
|
- Reduces Discovery time
|
||||||
|
- Standardizes deliveries
|
||||||
|
- Facilitates sales (ready presentation)
|
||||||
|
- Allows execution by different teams
|
||||||
|
|
||||||
|
### Why separate into 5 phases?
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Client can approve phase by phase
|
||||||
|
- Allows adjustments during process
|
||||||
|
- Facilitates risk management
|
||||||
|
- Incremental deliveries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. UX/Presentation Matters for Sales
|
||||||
|
|
||||||
|
The visual presentation made by the UX manager was **crucial** to closing both contracts. Good technical process + poor presentation = no sales.
|
||||||
|
|
||||||
|
### 2. Process Sells, Not Just Execution
|
||||||
|
|
||||||
|
Creating a **documented methodology** has more commercial value than just offering "consulting hours."
|
||||||
|
|
||||||
|
### 3. Each Client is Unique
|
||||||
|
|
||||||
|
The client requested **process restructuring**. A good process should be:
|
||||||
|
- Structured enough to be replicable
|
||||||
|
- Flexible enough to customize
|
||||||
|
|
||||||
|
### 4. Multidisciplinary Collaboration
|
||||||
|
|
||||||
|
Working with UX manager (presentations) + sales team (sales) + technical (execution) = success.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**Future opportunities:**
|
||||||
|
|
||||||
|
1. **Expansion:** Offer CNPJ Fast to more sectors (banks, fintechs, retail)
|
||||||
|
2. **Product:** Transform into automated tool (SaaS)
|
||||||
|
3. **Training:** Enable clients' internal teams
|
||||||
|
4. **Evolution:** Adapt process for other regulatory migrations (PIX, Open Banking)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Result:** Structured methodology that became a sellable product, generating revenue before the first technical execution.
|
||||||
|
|
||||||
|
[Want to implement CNPJ Fast in your company? Get in touch](#contact)
|
||||||
469
Content/Cases/en/cnpj-migration-database.md
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
---
|
||||||
|
title: "Alphanumeric CNPJ Migration - 100 Million Records"
|
||||||
|
slug: "cnpj-migration-database"
|
||||||
|
summary: "Execution of massive CNPJ migration from numeric to alphanumeric in database with ~100M records, using phased commit strategy to avoid database locks."
|
||||||
|
client: "Collection Agency"
|
||||||
|
industry: "Collections & Financial Services"
|
||||||
|
timeline: "In execution"
|
||||||
|
role: "Database Architect & Tech Lead"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- SQL Server
|
||||||
|
- Database Migration
|
||||||
|
- CNPJ
|
||||||
|
- Performance Optimization
|
||||||
|
- Batch Processing
|
||||||
|
- Big Data
|
||||||
|
featured: true
|
||||||
|
order: 4
|
||||||
|
date: 2024-11-01
|
||||||
|
seo_title: "Alphanumeric CNPJ Migration - 100M Records | Carneiro Tech"
|
||||||
|
seo_description: "Case study of massive CNPJ migration in database with 100 million records using phased commits and performance optimizations."
|
||||||
|
seo_keywords: "database migration, SQL Server, CNPJ, batch processing, performance optimization, phased commits"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
A collection agency that works with transitory data databases (no proprietary software) needs to adapt its systems to the new Brazilian **alphanumeric CNPJ** format.
|
||||||
|
|
||||||
|
**Main challenge:** Migrate ~**100 million records** in tables with `BIGINT` and `NUMERIC` columns to `VARCHAR`, without locking the production database.
|
||||||
|
|
||||||
|
**Status:** Project in execution (migration script preparation).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Massive Data Volume
|
||||||
|
|
||||||
|
**Company context:**
|
||||||
|
- Collection agency (does not develop proprietary software)
|
||||||
|
- Works with **transitory data** (high turnover)
|
||||||
|
- SQL Server database with critical volume
|
||||||
|
|
||||||
|
**Initial analysis revealed:**
|
||||||
|
|
||||||
|
| Table | Column | Current Type | Records | Size |
|
||||||
|
|--------|--------|------------|-----------|---------|
|
||||||
|
| Debtors | CNPJ_Debtor | BIGINT | 8,000,000 | 60 GB |
|
||||||
|
| Transactions | CNPJ_Payer | NUMERIC(14) | 90,000,000 | 1.2 TB |
|
||||||
|
| Companies | CNPJ_Company | BIGINT | 2,500,000 | 18 GB |
|
||||||
|
| **TOTAL** | - | - | **~100,000,000** | **~1.3 TB** |
|
||||||
|
|
||||||
|
**Identified problems:**
|
||||||
|
|
||||||
|
1. **Tables with 8M+ rows** using `BIGINT` for CNPJ
|
||||||
|
2. **90 million records** in transactions table
|
||||||
|
3. **CNPJ as primary key** in some tables
|
||||||
|
4. **Foreign keys** relating multiple tables
|
||||||
|
5. **Impossibility of extended downtime** (24/7 operation)
|
||||||
|
6. **Disk space restrictions** (requires efficient strategy)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Strategic Decision: Phased Commits
|
||||||
|
|
||||||
|
### Why NOT do ALTER COLUMN directly?
|
||||||
|
|
||||||
|
**Naive approach (DOESN'T work):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- NEVER DO THIS ON LARGE TABLES
|
||||||
|
ALTER TABLE Transactions
|
||||||
|
ALTER COLUMN CNPJ_Payer VARCHAR(18);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problems:**
|
||||||
|
- Locks entire table during conversion
|
||||||
|
- Can take hours/days on large tables
|
||||||
|
- Blocks all operations (INSERT, UPDATE, SELECT)
|
||||||
|
- Risk of timeout or failure mid-operation
|
||||||
|
- Complex rollback if something goes wrong
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Chosen Strategy: Column Swap with Phased Commits
|
||||||
|
|
||||||
|
**Based on previous experience**, I decided to use a gradual approach:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 1. Create new VARCHAR column at END │
|
||||||
|
│ (fast operation, doesn't lock table) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 2. UPDATE in batches (phased commits) │
|
||||||
|
│ - 100k records at a time │
|
||||||
|
│ - Pause between batches (avoid lock) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 3. Remove PKs and FKs │
|
||||||
|
│ (after 100% migrated) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 4. Rename columns (swap) │
|
||||||
|
│ - CNPJ → CNPJ_Old │
|
||||||
|
│ - CNPJ_New → CNPJ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 5. Recreate PKs/FKs with new column │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 6. Validation and old column deletion │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this approach?**
|
||||||
|
|
||||||
|
**No complete table lock** (incremental operation)
|
||||||
|
**Can pause/resume** at any time
|
||||||
|
**Real-time progress monitoring**
|
||||||
|
**Simple rollback** (just drop new column)
|
||||||
|
**Minimizes production impact** (small commits)
|
||||||
|
|
||||||
|
**Decision based on:**
|
||||||
|
- Previous experience with large volume migrations
|
||||||
|
- Knowledge of SQL Server locks
|
||||||
|
- Need for zero downtime
|
||||||
|
|
||||||
|
**Note:** This decision was made **without consulting AI** - based purely on practical experience from previous projects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Phase 1: Create New Column
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Fast operation (metadata change only)
|
||||||
|
ALTER TABLE Transactions
|
||||||
|
ADD CNPJ_Payer_New VARCHAR(18) NULL;
|
||||||
|
|
||||||
|
-- Add temporary index to speed up lookups
|
||||||
|
CREATE NONCLUSTERED INDEX IX_Temp_CNPJ_New
|
||||||
|
ON Transactions(CNPJ_Payer_New)
|
||||||
|
WHERE CNPJ_Payer_New IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estimated time:** ~1 second (independent of table size)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Batch Migration (Core Strategy)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Migration script with phased commits
|
||||||
|
DECLARE @BatchSize INT = 100000; -- 100k records per batch
|
||||||
|
DECLARE @RowsAffected INT = 1;
|
||||||
|
DECLARE @TotalProcessed INT = 0;
|
||||||
|
DECLARE @StartTime DATETIME = GETDATE();
|
||||||
|
|
||||||
|
WHILE @RowsAffected > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Update batch of 100k records not yet migrated
|
||||||
|
UPDATE TOP (@BatchSize) Transactions
|
||||||
|
SET CNPJ_Payer_New = RIGHT('00000000000000' + CAST(CNPJ_Payer AS VARCHAR), 14)
|
||||||
|
WHERE CNPJ_Payer_New IS NULL;
|
||||||
|
|
||||||
|
SET @RowsAffected = @@ROWCOUNT;
|
||||||
|
SET @TotalProcessed = @TotalProcessed + @RowsAffected;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
|
||||||
|
-- Progress log
|
||||||
|
PRINT 'Processed: ' + CAST(@TotalProcessed AS VARCHAR) + ' rows. Batch: ' + CAST(@RowsAffected AS VARCHAR);
|
||||||
|
PRINT 'Elapsed time: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR) + ' seconds';
|
||||||
|
|
||||||
|
-- Pause between batches (reduces contention)
|
||||||
|
WAITFOR DELAY '00:00:01'; -- 1 second between batches
|
||||||
|
END;
|
||||||
|
|
||||||
|
PRINT 'Migration completed! Total rows: ' + CAST(@TotalProcessed AS VARCHAR);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configurable parameters:**
|
||||||
|
|
||||||
|
- `@BatchSize`: 100k (balanced between performance and lock time)
|
||||||
|
- Too small = many transactions, overhead
|
||||||
|
- Too large = prolonged lock, production impact
|
||||||
|
- `WAITFOR DELAY`: 1 second (gives time for other queries to run)
|
||||||
|
|
||||||
|
**Time estimates:**
|
||||||
|
|
||||||
|
| Records | Batch Size | Estimated Time |
|
||||||
|
|-----------|------------|----------------|
|
||||||
|
| 8,000,000 | 100,000 | ~2-3 hours |
|
||||||
|
| 90,000,000 | 100,000 | ~20-24 hours |
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Doesn't freeze application
|
||||||
|
- Other queries can run between batches
|
||||||
|
- Can pause (Ctrl+C) and resume later (WHERE NULL picks up where it left off)
|
||||||
|
- Real-time progress log
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Constraint Removal
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Identifies all PKs and FKs involving the column
|
||||||
|
SELECT name
|
||||||
|
FROM sys.key_constraints
|
||||||
|
WHERE type = 'PK'
|
||||||
|
AND parent_object_id = OBJECT_ID('Transactions')
|
||||||
|
AND COL_NAME(parent_object_id, parent_column_id) = 'CNPJ_Payer';
|
||||||
|
|
||||||
|
-- Remove PKs
|
||||||
|
ALTER TABLE Transactions
|
||||||
|
DROP CONSTRAINT PK_Transactions_CNPJ;
|
||||||
|
|
||||||
|
-- Remove FKs (tables that reference)
|
||||||
|
ALTER TABLE Payments
|
||||||
|
DROP CONSTRAINT FK_Payments_Transactions;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estimated time:** ~10 minutes (depends on how many constraints exist)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Column Swap (Renaming)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Rename old column to _Old
|
||||||
|
EXEC sp_rename 'Transactions.CNPJ_Payer', 'CNPJ_Payer_Old', 'COLUMN';
|
||||||
|
|
||||||
|
-- Rename new column to original name
|
||||||
|
EXEC sp_rename 'Transactions.CNPJ_Payer_New', 'CNPJ_Payer', 'COLUMN';
|
||||||
|
|
||||||
|
-- Change to NOT NULL (after validating 100% populated)
|
||||||
|
ALTER TABLE Transactions
|
||||||
|
ALTER COLUMN CNPJ_Payer VARCHAR(18) NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estimated time:** ~1 second (metadata change)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 5: Constraint Recreation
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recreate PK with new VARCHAR column
|
||||||
|
ALTER TABLE Transactions
|
||||||
|
ADD CONSTRAINT PK_Transactions_CNPJ
|
||||||
|
PRIMARY KEY CLUSTERED (CNPJ_Payer);
|
||||||
|
|
||||||
|
-- Recreate FKs
|
||||||
|
ALTER TABLE Payments
|
||||||
|
ADD CONSTRAINT FK_Payments_Transactions
|
||||||
|
FOREIGN KEY (CNPJ_Payer) REFERENCES Transactions(CNPJ_Payer);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Estimated time:** ~30-60 minutes (depends on volume)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 6: Validation and Cleanup
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Validate that 100% was migrated
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM Transactions
|
||||||
|
WHERE CNPJ_Payer IS NULL OR CNPJ_Payer = '';
|
||||||
|
|
||||||
|
-- Validate referential integrity
|
||||||
|
DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS;
|
||||||
|
|
||||||
|
-- If everything OK, remove old column
|
||||||
|
ALTER TABLE Transactions
|
||||||
|
DROP COLUMN CNPJ_Payer_Old;
|
||||||
|
|
||||||
|
-- Remove temporary index
|
||||||
|
DROP INDEX IX_Temp_CNPJ_New ON Transactions;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CNPJ Fast Process Customization
|
||||||
|
|
||||||
|
### Differences vs. Original Process
|
||||||
|
|
||||||
|
The original **CNPJ Fast** process was **restructured** for this client:
|
||||||
|
|
||||||
|
**Main changes:**
|
||||||
|
|
||||||
|
| Aspect | Original CNPJ Fast | Client (Customized) |
|
||||||
|
|---------|-------------------|---------------------|
|
||||||
|
| **Focus** | Applications + DB | DB only (no proprietary software) |
|
||||||
|
| **Discovery** | App inventory | Schema analysis only |
|
||||||
|
| **Execution** | Multiple applications | Massive SQL scripts |
|
||||||
|
| **Batch Size** | 50k-100k | 100k (optimized for volume) |
|
||||||
|
| **Monitoring** | Manual + tools | Real-time SQL logs |
|
||||||
|
| **Rollback** | Complex process | Simple (DROP COLUMN) |
|
||||||
|
|
||||||
|
**Reason for restructuring:**
|
||||||
|
- Client has no proprietary applications (only consumes data)
|
||||||
|
- 100% focus on database optimization
|
||||||
|
- Much larger volume than typical cases (100M vs ~10M)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`SQL Server` `T-SQL` `Batch Processing` `Performance Tuning` `Database Optimization` `Migration Scripts` `Phased Commits` `Index Optimization` `Constraint Management`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Why 100k per batch?
|
||||||
|
|
||||||
|
**Performance tests:**
|
||||||
|
|
||||||
|
| Batch Size | Time/Batch | Lock Duration | Contention |
|
||||||
|
|------------|-------------|---------------|-----------|
|
||||||
|
| 10,000 | 2s | Low | Minimal |
|
||||||
|
| 50,000 | 8s | Medium | Acceptable |
|
||||||
|
| **100,000** | 15s | **Medium** | **Balanced** |
|
||||||
|
| 500,000 | 90s | High | Production impact |
|
||||||
|
| 1,000,000 | 180s | Very high | Unacceptable |
|
||||||
|
|
||||||
|
**Choice:** 100k offers best balance between performance and impact.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why create column at END?
|
||||||
|
|
||||||
|
**SQL Server internals:**
|
||||||
|
- Add column at end = metadata change (fast)
|
||||||
|
- Add in middle = page rewrite (slow)
|
||||||
|
- For large tables, position matters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why WAITFOR DELAY of 1 second?
|
||||||
|
|
||||||
|
**Without delay:**
|
||||||
|
- Batch processing consumes 100% of I/O
|
||||||
|
- Application queries slow down
|
||||||
|
- Lock escalation may occur
|
||||||
|
|
||||||
|
**With 1s delay:**
|
||||||
|
- Other queries have window to execute
|
||||||
|
- Distributed I/O
|
||||||
|
- User experience preserved
|
||||||
|
|
||||||
|
**Trade-off:** Migration takes +1s per batch (~25% slower), but system remains responsive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Status & Next Steps
|
||||||
|
|
||||||
|
### Current Status (December 2024)
|
||||||
|
|
||||||
|
**Preparation Phase:**
|
||||||
|
- Discovery complete (100M records identified)
|
||||||
|
- Migration scripts developed
|
||||||
|
- Tests in staging environment
|
||||||
|
- Performance validation in progress
|
||||||
|
- Awaiting production maintenance window
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
1. **Complete production backup**
|
||||||
|
2. **Production execution** (24/7 environment)
|
||||||
|
3. **Real-time monitoring** during migration
|
||||||
|
4. **Post-migration validation** (integrity, performance)
|
||||||
|
5. **Lessons learned documentation**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned (So Far)
|
||||||
|
|
||||||
|
### 1. Previous Experience is Gold
|
||||||
|
|
||||||
|
Decision to use phased commits came from **practical experience** in previous projects, not from documentation or AI.
|
||||||
|
|
||||||
|
**Similar previous situations:**
|
||||||
|
- E-commerce data migration (50M records)
|
||||||
|
- Encoding conversion (UTF-8 in 100M+ rows)
|
||||||
|
- Historical table partitioning
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. "Measure Twice, Cut Once"
|
||||||
|
|
||||||
|
Before executing in production:
|
||||||
|
- Exhaustive tests in staging
|
||||||
|
- Scripts validated and reviewed
|
||||||
|
- Rollback tested
|
||||||
|
- Time estimates confirmed
|
||||||
|
|
||||||
|
**Preparation time:** 3 weeks
|
||||||
|
**Execution time:** Estimated at 48 hours
|
||||||
|
|
||||||
|
**Ratio:** 10:1 (preparation vs execution)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Customization > One-Size-Fits-All
|
||||||
|
|
||||||
|
The original CNPJ Fast process needed to be **restructured** for this client.
|
||||||
|
|
||||||
|
**Lesson:** Processes should be:
|
||||||
|
- Structured enough to repeat
|
||||||
|
- Flexible enough to adapt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Monitoring is Crucial
|
||||||
|
|
||||||
|
Scripts with **detailed progress logs** allow:
|
||||||
|
- Estimate remaining time
|
||||||
|
- Identify bottlenecks
|
||||||
|
- Pause/resume with confidence
|
||||||
|
- Report status to stakeholders
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Log example
|
||||||
|
Processed: 10,000,000 rows. Batch: 100,000
|
||||||
|
Elapsed time: 3600 seconds (10% complete, ~9h remaining)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
### Optimizations Implemented
|
||||||
|
|
||||||
|
1. **Temporary index WHERE NULL**
|
||||||
|
- Speeds up lookup of unmigrated records
|
||||||
|
- Removed after completion
|
||||||
|
|
||||||
|
2. **Optimized batch size**
|
||||||
|
- Balanced between performance and lock time
|
||||||
|
|
||||||
|
3. **Transaction log management**
|
||||||
|
```sql
|
||||||
|
-- Check log growth
|
||||||
|
DBCC SQLPERF(LOGSPACE);
|
||||||
|
|
||||||
|
-- Adjust recovery model (if allowed)
|
||||||
|
ALTER DATABASE MyDatabase SET RECOVERY SIMPLE;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Execution during low-load hours**
|
||||||
|
- Overnight maintenance window
|
||||||
|
- Weekend (if possible)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Expected result:** Migration of 100 million records in ~48 hours, without significant downtime and with possibility of fast rollback.
|
||||||
|
|
||||||
|
[Need to migrate massive data volumes? Get in touch](#contact)
|
||||||
588
Content/Cases/en/industrial-learning-platform.md
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
---
|
||||||
|
title: "Industrial Training Platform - From Wireframes to Complete System"
|
||||||
|
slug: "industrial-learning-platform"
|
||||||
|
summary: "Solution Design for microlearning platform in industrial gas company. Identification of critical unmapped requirements (admin, registrations, exports) before client presentation, avoiding rework and ensuring real usability."
|
||||||
|
client: "Industrial Gas Company"
|
||||||
|
industry: "Industrial & Manufacturing"
|
||||||
|
timeline: "4 months"
|
||||||
|
role: "Solution Architect & Tech Lead"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- Solution Design
|
||||||
|
- EdTech
|
||||||
|
- Learning Platform
|
||||||
|
- Requirements Analysis
|
||||||
|
- Tech Lead
|
||||||
|
- User Stories
|
||||||
|
- .NET
|
||||||
|
- Product Design
|
||||||
|
featured: true
|
||||||
|
order: 5
|
||||||
|
date: 2024-06-01
|
||||||
|
seo_title: "Industrial Training Platform - Solution Design"
|
||||||
|
seo_description: "Case study of Solution Design for microlearning platform, identifying critical requirements before client presentation and leading development to production."
|
||||||
|
seo_keywords: "solution design, learning platform, microlearning, requirements analysis, tech lead, industrial training"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Industrial gas company requests platform to train employees using **microlearning** methodology (short and objective content).
|
||||||
|
|
||||||
|
**Initial requirement:** "We just want the structure - track, microlearning, test question and score."
|
||||||
|
|
||||||
|
**Problem:** Incomplete specification that would result in a system **impossible to use** (no way to register content, no administrators, no export of results).
|
||||||
|
|
||||||
|
**Solution:** Critical requirements analysis **before client presentation**, identifying functional gaps and proposing complete solution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Beautiful Wireframes, Incomplete Functionality
|
||||||
|
|
||||||
|
**Initial situation:**
|
||||||
|
|
||||||
|
UX created beautiful wireframes showing:
|
||||||
|
- Learning tracks
|
||||||
|
- Microlearnings (video/text + image)
|
||||||
|
- Test questions (multiple choice)
|
||||||
|
- Score per employee
|
||||||
|
|
||||||
|
**Identified problem:**
|
||||||
|
|
||||||
|
Nobody (client, UX, commercial) thought about:
|
||||||
|
|
||||||
|
**How does content enter the system?**
|
||||||
|
- Who registers tracks?
|
||||||
|
- Who creates microlearnings?
|
||||||
|
- Who writes questions?
|
||||||
|
- Manual interface or import?
|
||||||
|
|
||||||
|
**Who manages the system?**
|
||||||
|
- Is there admin concept?
|
||||||
|
- Can HR create admins?
|
||||||
|
- Can area manager see only their team?
|
||||||
|
|
||||||
|
**How does data leave the system?**
|
||||||
|
- HR needs reports
|
||||||
|
- Compliance needs evidence
|
||||||
|
- How to export data?
|
||||||
|
- Format: Excel? PDF? API?
|
||||||
|
|
||||||
|
**Real risk:**
|
||||||
|
|
||||||
|
If we developed exactly what was requested:
|
||||||
|
- System would work technically
|
||||||
|
- **But would be completely unusable**
|
||||||
|
- Client would have to pay for rework to include basic CRUD
|
||||||
|
- Rework + additional cost + frustration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Design Process
|
||||||
|
|
||||||
|
### Step 1: Critical Analysis (Before Presentation)
|
||||||
|
|
||||||
|
**Action taken:** Called meeting with UX **before** presenting to client.
|
||||||
|
|
||||||
|
**Points raised:**
|
||||||
|
|
||||||
|
**"How does the first content enter the system?"**
|
||||||
|
- UX: "Ah... we didn't think about that. Will you populate the database?"
|
||||||
|
- Me: "And when client wants to add new track? Will we modify production database?"
|
||||||
|
|
||||||
|
**"Who is the system owner?"**
|
||||||
|
- UX: "HR, I imagine."
|
||||||
|
- Me: "Just one person? What if they leave the company? How do they delegate?"
|
||||||
|
|
||||||
|
**"Did HR ask for reports?"**
|
||||||
|
- UX: "It wasn't mentioned in the briefing."
|
||||||
|
- Me: "HR always needs reports. It's for compliance (NR, ISO)."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Identified Functional Requirements
|
||||||
|
|
||||||
|
I proposed 4 additional **essential** modules:
|
||||||
|
|
||||||
|
#### 1. Administration System
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Standard user: Only takes training
|
||||||
|
- Admin user: Manages content + sees reports
|
||||||
|
- Admin can promote other users to admin
|
||||||
|
- Access control (general admin vs area admin)
|
||||||
|
|
||||||
|
**Why it's critical:**
|
||||||
|
System is static without this (content never updates).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Content CRUD
|
||||||
|
|
||||||
|
**a) Track Registration:**
|
||||||
|
- Track name
|
||||||
|
- Description
|
||||||
|
- Microlearning order
|
||||||
|
- Active/inactive track (allows unpublishing)
|
||||||
|
|
||||||
|
**b) Microlearning Registration:**
|
||||||
|
- Title
|
||||||
|
- Type: Simple text (2 paragraphs) OR Video
|
||||||
|
- Image upload (if text)
|
||||||
|
- Video URL (if video)
|
||||||
|
- Order within track
|
||||||
|
|
||||||
|
**c) Question Registration:**
|
||||||
|
- Question (text)
|
||||||
|
- 3 answer options:
|
||||||
|
- "Great" (green)
|
||||||
|
- "So-so" (yellow)
|
||||||
|
- "Poor" (red)
|
||||||
|
- Points per answer (e.g., 10, 5, 0 points)
|
||||||
|
- Custom feedback per answer
|
||||||
|
|
||||||
|
**Why it's critical:**
|
||||||
|
Client needs to update content without calling dev/DBA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Data Export
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Export to Excel (.xlsx)
|
||||||
|
- Filters:
|
||||||
|
- By period (start/end date)
|
||||||
|
- By track
|
||||||
|
- By employee
|
||||||
|
- By area/department
|
||||||
|
- Exported columns:
|
||||||
|
- Employee name
|
||||||
|
- ID number
|
||||||
|
- Completed track
|
||||||
|
- Total score
|
||||||
|
- Completion date
|
||||||
|
- Individual answers (for audit)
|
||||||
|
|
||||||
|
**Why it's critical:**
|
||||||
|
HR needs to evidence training for:
|
||||||
|
- Regulatory Norms (NR-13, NR-20 - flammable gases)
|
||||||
|
- ISO audits
|
||||||
|
- Labor lawsuits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. User Management
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Import employees (CSV/Excel upload)
|
||||||
|
- Manual registration
|
||||||
|
- Activate/deactivate users
|
||||||
|
- Assign mandatory tracks by area
|
||||||
|
- Pending notifications
|
||||||
|
|
||||||
|
**Why it's critical:**
|
||||||
|
Company has 500+ employees, manual registration is unfeasible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Client Presentation
|
||||||
|
|
||||||
|
**Approach:**
|
||||||
|
|
||||||
|
1. Showed UX wireframes (beautiful interface)
|
||||||
|
2. Asked: "How will you register the first track?"
|
||||||
|
3. Client: "Ah... good question. We hadn't thought about that."
|
||||||
|
4. Presented the 4 additional modules
|
||||||
|
5. Client: "Makes total sense! Without this we can't use it."
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
- Proposal approved **with additional modules**
|
||||||
|
- Adjusted scope (timeline + budget)
|
||||||
|
- Zero future rework
|
||||||
|
- Client recognized added value
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### My Role in the Project
|
||||||
|
|
||||||
|
**1. Solution Architect**
|
||||||
|
- Identification of non-functional requirements
|
||||||
|
- Architecture design (modules, integrations)
|
||||||
|
- Technology definition
|
||||||
|
|
||||||
|
**2. Tech Lead**
|
||||||
|
- Technical team leadership (3 devs)
|
||||||
|
- Code review
|
||||||
|
- Code standards definition
|
||||||
|
- Technical risk management
|
||||||
|
|
||||||
|
**3. Technical Product Owner**
|
||||||
|
- Creation of complete **user stories**
|
||||||
|
- Backlog prioritization
|
||||||
|
- Continuous refinement with client
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Chosen Tech Stack
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `.NET 7` - REST APIs
|
||||||
|
- `Entity Framework Core` - ORM
|
||||||
|
- `SQL Server` - Database
|
||||||
|
- `ClosedXML` - Excel generation
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `React` - Web interface
|
||||||
|
- `Material-UI` - Components
|
||||||
|
- `React Player` - Video player
|
||||||
|
- `Chart.js` - Progress charts
|
||||||
|
|
||||||
|
**Infrastructure:**
|
||||||
|
- `Azure App Service` - Hosting
|
||||||
|
- `Azure Blob Storage` - Video/image storage
|
||||||
|
- `Azure SQL Database` - Managed database
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Created User Stories
|
||||||
|
|
||||||
|
I wrote **32 user stories** covering all flows. Examples:
|
||||||
|
|
||||||
|
**US-01: Register Track (Admin)**
|
||||||
|
```
|
||||||
|
As system administrator
|
||||||
|
I want to register a new training track
|
||||||
|
So that employees can take the courses
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- Admin accesses "Tracks" menu → "New Track"
|
||||||
|
- Fills in: Name, Description, Status (Active/Inactive)
|
||||||
|
- Can add existing microlearnings to track
|
||||||
|
- Defines microlearning order (drag & drop)
|
||||||
|
- System validates mandatory fields
|
||||||
|
- Saves and displays success message
|
||||||
|
```
|
||||||
|
|
||||||
|
**US-15: Complete Microlearning (Employee)**
|
||||||
|
```
|
||||||
|
As employee
|
||||||
|
I want to complete a microlearning from my track
|
||||||
|
To learn about the topic and earn points
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- Employee accesses assigned track
|
||||||
|
- Sees list of microlearnings (uncompleted first)
|
||||||
|
- Clicks microlearning → opens screen with:
|
||||||
|
- Text (2 paragraphs) + Image OR
|
||||||
|
- Embedded video player
|
||||||
|
- "Continue" button appears after:
|
||||||
|
- 30s (if text)
|
||||||
|
- End of video (if video)
|
||||||
|
- Marks microlearning as seen
|
||||||
|
- Test question appears automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
**US-22: Export Results (Admin)**
|
||||||
|
```
|
||||||
|
As administrator
|
||||||
|
I want to export training results to Excel
|
||||||
|
To generate compliance and audit reports
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- Admin accesses "Reports" → "Export"
|
||||||
|
- Selects filters (period, track, area)
|
||||||
|
- Clicks "Generate Excel"
|
||||||
|
- System processes and downloads .xlsx file
|
||||||
|
- Excel contains columns: Name, ID, Track, Points, Date, Answers
|
||||||
|
- Readable format (bold headers, auto-adjusted columns)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features Implemented
|
||||||
|
|
||||||
|
### 1. Gamified Scoring System
|
||||||
|
|
||||||
|
**Mechanics:**
|
||||||
|
- Each question worth points (configurable)
|
||||||
|
- "Great" answer: 10 points
|
||||||
|
- "So-so" answer: 5 points
|
||||||
|
- "Poor" answer: 0 points
|
||||||
|
|
||||||
|
**Employee dashboard:**
|
||||||
|
- Total score
|
||||||
|
- Ranking (optional, configurable)
|
||||||
|
- Badges for completed tracks
|
||||||
|
- Visual progress (% bar)
|
||||||
|
|
||||||
|
**Why it works:**
|
||||||
|
Factory floor employees engage more with gamification elements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Adaptive Microlearning
|
||||||
|
|
||||||
|
**Content types:**
|
||||||
|
|
||||||
|
**Text + Image:**
|
||||||
|
- 2 paragraphs (max 300 words)
|
||||||
|
- 1 illustrative image
|
||||||
|
- Ideal for: Procedures, norms, concepts
|
||||||
|
|
||||||
|
**Video:**
|
||||||
|
- Short videos (2-5 min)
|
||||||
|
- Embedded player (YouTube/Vimeo or upload)
|
||||||
|
- Ideal for: Demonstrations, equipment operations
|
||||||
|
|
||||||
|
**Why microlearning?**
|
||||||
|
- Employees complete during breaks (10-15min)
|
||||||
|
- Short content = higher retention
|
||||||
|
- Facilitates updates (vs long courses)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Delegated Administration System
|
||||||
|
|
||||||
|
**Hierarchy:**
|
||||||
|
|
||||||
|
```
|
||||||
|
General Admin (HR)
|
||||||
|
↓ can promote
|
||||||
|
Area Admin (Managers)
|
||||||
|
↓ can view only
|
||||||
|
Employees from their area
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permissions:**
|
||||||
|
- General admin: Creates tracks, promotes admins, sees all data
|
||||||
|
- Area admin: Sees only their area reports
|
||||||
|
- Employee: Only takes training
|
||||||
|
|
||||||
|
**Audit:**
|
||||||
|
- Logs of who created/edited each content
|
||||||
|
- History of admin promotions
|
||||||
|
- SOX/ISO compliance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Export for Compliance
|
||||||
|
|
||||||
|
**Generated Excel format:**
|
||||||
|
|
||||||
|
| ID | Name | Area | Track | Completion Date | Points | Status |
|
||||||
|
|-----------|------|------|--------|----------------|--------|--------|
|
||||||
|
| 1001 | John Silva | Production | NR-20 Safety | 11/15/2024 | 95/100 | Approved |
|
||||||
|
| 1002 | Mary Santos | Logistics | Gas Handling | 11/14/2024 | 78/100 | Approved |
|
||||||
|
|
||||||
|
**Additional sheet: Answer Details**
|
||||||
|
- Allows audit: "Did employee X answer question Y correctly?"
|
||||||
|
- Evidence for labor lawsuits
|
||||||
|
- NR-13/NR-20 compliance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### System in Production
|
||||||
|
|
||||||
|
**Current status:** In use for 4+ months
|
||||||
|
|
||||||
|
**Adoption metrics:**
|
||||||
|
- 500+ registered employees
|
||||||
|
- 12 active tracks
|
||||||
|
- 150+ created microlearnings
|
||||||
|
- 8,000+ completed training sessions
|
||||||
|
- 100+ exported reports (compliance)
|
||||||
|
|
||||||
|
**Completion rate:** 87% (industry average: 45%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Client Impact
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
- In-person training (high cost, difficult scheduling)
|
||||||
|
- Paper evidence (losses, difficult audit)
|
||||||
|
- Difficulty updating content
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
- Asynchronous training (employee completes when possible)
|
||||||
|
- Digital evidence (facilitated compliance)
|
||||||
|
- HR updates content without calling IT
|
||||||
|
- 70% reduction in training cost
|
||||||
|
|
||||||
|
**Client feedback:**
|
||||||
|
> "If we had implemented only what we initially requested, the system would be useless. The pre-analysis saved the project."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Solution Design Value
|
||||||
|
|
||||||
|
**ROI of pre-sale analysis:**
|
||||||
|
|
||||||
|
**Scenario A (without analysis):**
|
||||||
|
1. Develop interface only (2 months)
|
||||||
|
2. Client tests and realizes CRUD is missing (1 month later)
|
||||||
|
3. Rework to add modules (2+ months)
|
||||||
|
4. **Total: 5+ months + client frustration**
|
||||||
|
|
||||||
|
**Scenario B (with analysis - what we did):**
|
||||||
|
1. Identify requirements beforehand (1 week)
|
||||||
|
2. Approve complete scope (1 week)
|
||||||
|
3. Develop correct solution (4 months)
|
||||||
|
4. **Total: 4 months + satisfied client**
|
||||||
|
|
||||||
|
**Savings:** 1+ month of rework + opportunity cost
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`.NET 7` `C#` `Entity Framework Core` `SQL Server` `React` `Material-UI` `Azure App Service` `Azure Blob Storage` `ClosedXML` `Chart.js` `User Stories` `Solution Design` `Tech Lead`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Why not use ready-made LMS? (Moodle, Canvas)
|
||||||
|
|
||||||
|
**Alternatives considered:**
|
||||||
|
1. Moodle (open-source, free)
|
||||||
|
2. Totara/Canvas (corporate LMS)
|
||||||
|
3. **Custom development**
|
||||||
|
|
||||||
|
**Justification:**
|
||||||
|
- Generic LMS: Unnecessary complexity (forums, wikis, etc)
|
||||||
|
- Client wants **only microlearning** (simplicity)
|
||||||
|
- LMS license cost > custom dev cost
|
||||||
|
- Client AD/SSO integration (easier custom)
|
||||||
|
- UX optimized for factory floor (mobile-first, touch)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why 3 answer options (vs 4-5)?
|
||||||
|
|
||||||
|
**Choice:** Green (Great), Yellow (So-so), Red (Poor)
|
||||||
|
|
||||||
|
**Justification:**
|
||||||
|
- Factory floor employees prefer simplicity
|
||||||
|
- Universal colors (traffic light)
|
||||||
|
- Avoids choice paradox (fewer options = more engagement)
|
||||||
|
- Clearer gamification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why Excel Export (vs Online Dashboard)?
|
||||||
|
|
||||||
|
**Both were implemented**, but Excel is critical for:
|
||||||
|
|
||||||
|
**Regulatory compliance:**
|
||||||
|
- Auditors ask for "digitally signed file"
|
||||||
|
- NR-13/NR-20 require physical evidence
|
||||||
|
- Labor lawsuits accept Excel
|
||||||
|
|
||||||
|
**Flexibility:**
|
||||||
|
- HR can do custom analyses in Excel
|
||||||
|
- Combine with other data sources
|
||||||
|
- Presentations for board
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Solution Design Prevents Rework
|
||||||
|
|
||||||
|
**Lesson:** 1 week of critical analysis saves months of rework.
|
||||||
|
|
||||||
|
**Application:**
|
||||||
|
- Always question incomplete specifications
|
||||||
|
- Think about "the day after" (who manages this in production?)
|
||||||
|
- Involve client in requirements discussions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. UX ≠ Functional Requirements
|
||||||
|
|
||||||
|
**Lesson:** Beautiful wireframes don't replace requirements analysis.
|
||||||
|
|
||||||
|
**UX focuses on:** How user **uses** the system
|
||||||
|
**Solution Design focuses on:** How system **works** end-to-end
|
||||||
|
|
||||||
|
Both are necessary and complementary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Asking "How?" is More Important than "What?"
|
||||||
|
|
||||||
|
**Client says:** "I want tracks and microlearnings"
|
||||||
|
**Solution Designer asks:** "How does the first track enter the system?"
|
||||||
|
|
||||||
|
This simple question revealed 4 missing modules.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Well-Written User Stories Accelerate Development
|
||||||
|
|
||||||
|
**Investment:** 2 weeks writing 32 detailed user stories
|
||||||
|
|
||||||
|
**Return:**
|
||||||
|
- Devs knew exactly what to build
|
||||||
|
- Zero ambiguity
|
||||||
|
- Very few bugs (clear requirements)
|
||||||
|
- Client validated stories before coding
|
||||||
|
|
||||||
|
**Lesson:** Time spent planning reduces development time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Compliance is Hidden Requirement
|
||||||
|
|
||||||
|
**In regulated industries** (health, energy, chemical), there will always be:
|
||||||
|
- Audit needs
|
||||||
|
- Evidence exports
|
||||||
|
- Logs of who did what
|
||||||
|
|
||||||
|
**Lesson:** Ask about compliance **before**, not after.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenges Overcome
|
||||||
|
|
||||||
|
| Challenge | Solution | Result |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| Incomplete specification | Pre-sale critical analysis | Correct scope from start |
|
||||||
|
| Client without technical knowledge | User stories in business language | Client validated requirements |
|
||||||
|
| Employees with low digital literacy | Simplified UX (3 buttons, colors) | 87% completion rate |
|
||||||
|
| NR-13/NR-20 compliance | Excel export with details | Approved in 2 audits |
|
||||||
|
| Managing 500+ users | CSV import + admin hierarchy | Onboarding in 1 week |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Future Roadmap)
|
||||||
|
|
||||||
|
**Planned features:**
|
||||||
|
|
||||||
|
1. **Push Notifications**
|
||||||
|
- Remind employee of pending training
|
||||||
|
- Notify of new mandatory track
|
||||||
|
|
||||||
|
2. **Native Mobile App**
|
||||||
|
- Offline-first (downloaded videos)
|
||||||
|
- Employees without computer
|
||||||
|
|
||||||
|
3. **Digital Certificates**
|
||||||
|
- Digitally signed PDF
|
||||||
|
- QR code for validation
|
||||||
|
|
||||||
|
4. **Data Intelligence**
|
||||||
|
- Which microlearnings have most errors?
|
||||||
|
- Identify knowledge gaps by area
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Result:** Functional system in production, satisfied client, zero rework - all because 1 week was invested in **thinking before coding**.
|
||||||
|
|
||||||
|
[Need critical requirements analysis? Get in touch](#contact)
|
||||||
577
Content/Cases/en/pharma-digital-transformation.md
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
---
|
||||||
|
title: "Pharma Lab Digital MVP - From Zero to Production"
|
||||||
|
slug: "pharma-digital-transformation"
|
||||||
|
summary: "Squad leadership in greenfield project for pharmaceutical lab, building digital platform MVP with complex integrations (Salesforce, Twilio, official APIs) starting from absolute zero - no Git, no servers, no infrastructure."
|
||||||
|
client: "Pharmaceutical Laboratory"
|
||||||
|
industry: "Pharmaceutical & Healthcare"
|
||||||
|
timeline: "4 months (2-month planned delay)"
|
||||||
|
role: "Tech Lead & Solution Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- MVP
|
||||||
|
- Digital Transformation
|
||||||
|
- .NET
|
||||||
|
- React
|
||||||
|
- Next.js
|
||||||
|
- Salesforce
|
||||||
|
- Twilio
|
||||||
|
- SQL Server
|
||||||
|
- Tech Lead
|
||||||
|
- Greenfield
|
||||||
|
featured: true
|
||||||
|
order: 3
|
||||||
|
date: 2023-03-01
|
||||||
|
seo_title: "Pharma Digital MVP - Digital Transformation from Scratch"
|
||||||
|
seo_description: "Case study of building digital MVP for pharmaceutical lab from scratch: no Git, no infrastructure, with complex integrations and successful delivery."
|
||||||
|
seo_keywords: "MVP, digital transformation, pharma, .NET, React, Next.js, Salesforce, greenfield project, tech lead"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Pharmaceutical laboratory at the **beginning of digital transformation** hires consulting firm to build discount platform for prescribing physicians, starting from WordPress prototype.
|
||||||
|
|
||||||
|
**Unique challenge:** Start greenfield project in company **without basic development infrastructure** - no Git, no provisioned servers, no defined processes.
|
||||||
|
|
||||||
|
**Context:** Project executed in multi-squad environment. **Successful production delivery** despite initial infrastructure challenges, with controlled 2-month delay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Digital Transformation... Starting from Absolute Zero
|
||||||
|
|
||||||
|
**Company initial state (2023):**
|
||||||
|
|
||||||
|
**No Git/versioning**
|
||||||
|
- Code only on local machines
|
||||||
|
- Non-existent history
|
||||||
|
- Impossible collaboration
|
||||||
|
|
||||||
|
**No provisioned servers**
|
||||||
|
- Non-existent development environment
|
||||||
|
- Staging not configured
|
||||||
|
- Production not prepared
|
||||||
|
|
||||||
|
**No development processes**
|
||||||
|
- No CI/CD
|
||||||
|
- No code review
|
||||||
|
- No structured task management
|
||||||
|
|
||||||
|
**No experienced internal technical team**
|
||||||
|
- Team unfamiliar with modern stacks
|
||||||
|
- First contact with React, REST APIs
|
||||||
|
- Inexperience with complex integrations
|
||||||
|
|
||||||
|
**Technical starting point:**
|
||||||
|
- Functional prototype in **WordPress**
|
||||||
|
- Content and texts already approved
|
||||||
|
- UX/UI defined
|
||||||
|
- Business rules documented (partially)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Required Complex Integrations
|
||||||
|
|
||||||
|
The MVP needed to integrate with multiple external systems:
|
||||||
|
|
||||||
|
1. **Salesforce** - Discount order registration
|
||||||
|
2. **Twilio** - SMS for login validation (2FA)
|
||||||
|
3. **Official physician API** - CRM validation + professional data
|
||||||
|
4. **Interplayers** - Discount record sending by CPF
|
||||||
|
5. **WordPress** - Content reading (headless CMS)
|
||||||
|
6. **SQL Server** - Data persistence
|
||||||
|
|
||||||
|
**Additional complexity:**
|
||||||
|
- Different credentials/environments per integration
|
||||||
|
- Varying SLAs (Twilio critical, WordPress tolerant)
|
||||||
|
- Provider-specific error handling
|
||||||
|
- LGPD compliance (sensitive physician data)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### Strategy: Start Small, Build Solid
|
||||||
|
|
||||||
|
**Initial decision:** Explain to the team the process we would follow, establishing foundations before coding.
|
||||||
|
|
||||||
|
### Phase 1: Basic Infrastructure Setup (Weeks 1-2)
|
||||||
|
|
||||||
|
Even without provisioned servers, I started essential setup:
|
||||||
|
|
||||||
|
**Git & Versioning:**
|
||||||
|
```bash
|
||||||
|
# Structured repository from day 1
|
||||||
|
git init
|
||||||
|
git flow init # Defined branch strategy
|
||||||
|
|
||||||
|
# Monorepo structure
|
||||||
|
/
|
||||||
|
├── frontend/ # Next.js + React
|
||||||
|
├── backend/ # .NET APIs
|
||||||
|
├── cms-adapter/ # WordPress integration
|
||||||
|
└── docs/ # Architecture and ADRs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Process explained to team:**
|
||||||
|
1. Everything in Git (atomic commits, descriptive messages)
|
||||||
|
2. Feature branches (never commit directly to main)
|
||||||
|
3. Mandatory code review (2 approvals)
|
||||||
|
4. CI/CD prepared (for when servers are ready)
|
||||||
|
|
||||||
|
**Local environments first:**
|
||||||
|
- Docker Compose for local development
|
||||||
|
- External API mocks (until credentials arrive)
|
||||||
|
- Local SQL Server with data seeds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Modern & Decoupled Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ FRONTEND (Next.js + React) │
|
||||||
|
│ - SSR for SEO │
|
||||||
|
│ - Client-side for interactivity │
|
||||||
|
│ - API consumption │
|
||||||
|
└────────────┬────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ BACKEND APIs (.NET 7) │
|
||||||
|
│ - REST APIs │
|
||||||
|
│ - Authentication/Authorization │
|
||||||
|
│ - Business logic │
|
||||||
|
│ - Orchestration layer │
|
||||||
|
└────┬────┬────┬────┬────┬─────────────────────────┬──┘
|
||||||
|
│ │ │ │ │ │
|
||||||
|
▼ ▼ ▼ ▼ ▼ ▼
|
||||||
|
┌────────┐ ┌──────┐ ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
|
||||||
|
│Salesf. │ │Twilio│ │CRM │ │Interpl.│ │WordPr. │ │SQL Server│
|
||||||
|
│ │ │ │ │API │ │ │ │(CMS) │ │ │
|
||||||
|
└────────┘ └──────┘ └──────┘ └────────┘ └────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Chosen stack:**
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `Next.js 13` - SSR, routing, optimizations
|
||||||
|
- `React 18` - Components, hooks, context
|
||||||
|
- `TypeScript` - Type safety
|
||||||
|
- `Tailwind CSS` - Modern styling
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `.NET 7` - REST APIs
|
||||||
|
- `Entity Framework Core` - ORM
|
||||||
|
- `SQL Server 2019` - Database
|
||||||
|
- `Polly` - Resilience patterns (retry, circuit breaker)
|
||||||
|
|
||||||
|
**Why Next.js instead of keeping WordPress?**
|
||||||
|
- Performance (SSR vs monolithic PHP)
|
||||||
|
- Optimized SEO (critical for pharma)
|
||||||
|
- Modern experience (SPA when needed)
|
||||||
|
- Scalability
|
||||||
|
- WordPress kept only as CMS (headless)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Integrations (Project Core)
|
||||||
|
|
||||||
|
#### 1. Salesforce - Campaigns and Order Registration
|
||||||
|
|
||||||
|
**Implemented solution:**
|
||||||
|
|
||||||
|
Salesforce was configured to manage two main functionalities:
|
||||||
|
|
||||||
|
**a) Discount campaigns:**
|
||||||
|
- Marketing configures campaigns in Salesforce (medication X, discount Y%, period)
|
||||||
|
- Backend queries active campaigns via API
|
||||||
|
- Frontend (Next.js) displays available discount percentage based on: medication + active campaign
|
||||||
|
|
||||||
|
**b) Order registration:**
|
||||||
|
- User informs: physician CRM, state, patient CPF, medication
|
||||||
|
- System validates data (real CRM via official API, valid CPF)
|
||||||
|
- Percentage calculated automatically (Salesforce campaigns + CMS rules)
|
||||||
|
- Order registered in Salesforce with all data (LGPD compliance)
|
||||||
|
|
||||||
|
**Overcome technical challenges:**
|
||||||
|
- OAuth2 authentication with automatic refresh token
|
||||||
|
- Rate limiting (Salesforce has API/day limits)
|
||||||
|
- Retry logic for transient failures (Polly)
|
||||||
|
- CPF masking for logs (LGPD)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Twilio - SMS Authentication (2FA)
|
||||||
|
|
||||||
|
**Implemented solution:**
|
||||||
|
|
||||||
|
Two-factor authentication system to ensure security:
|
||||||
|
|
||||||
|
**Login flow:**
|
||||||
|
1. User enters phone number
|
||||||
|
2. Backend generates 6-digit code (valid for 5 minutes)
|
||||||
|
3. SMS sent via Twilio ("Your code: 123456")
|
||||||
|
4. User enters code in frontend
|
||||||
|
5. Backend validates code + expiration timestamp
|
||||||
|
6. JWT token issued after successful validation
|
||||||
|
|
||||||
|
**Compliance and audit:**
|
||||||
|
- Phone numbers masked in logs (LGPD)
|
||||||
|
- Complete audit (who, when, which SMS)
|
||||||
|
- Delivery rate: 99.8%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Official Physician API (Regional Medical Council)
|
||||||
|
|
||||||
|
**Implemented solution:**
|
||||||
|
|
||||||
|
Automatic physician validation via official medical council API:
|
||||||
|
|
||||||
|
**Performed validations:**
|
||||||
|
- CRM exists and is active in council
|
||||||
|
- Physician name matches informed CRM
|
||||||
|
- Specialty is allowed (lab business rule)
|
||||||
|
- State corresponds to registration state
|
||||||
|
|
||||||
|
**Optimizations:**
|
||||||
|
- 24-hour cache to reduce official API calls
|
||||||
|
- Fallback if API is down (notifies admin)
|
||||||
|
- Automatic retry for transient failures
|
||||||
|
|
||||||
|
**Why this matters:**
|
||||||
|
Ensures only real and active physicians can prescribe discounts, avoiding fraud.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. WordPress as Headless CMS
|
||||||
|
|
||||||
|
**Implemented solution:**
|
||||||
|
|
||||||
|
Marketing continues managing content in WordPress (familiar), but frontend is modern Next.js.
|
||||||
|
|
||||||
|
**Architecture:**
|
||||||
|
- WordPress: Manages texts, images, campaign rules
|
||||||
|
- WordPress REST API: Exposes content via JSON
|
||||||
|
- Next.js: Consumes API and renders with SSR (SEO optimized)
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Marketing doesn't need to learn new tool
|
||||||
|
- Modern frontend (performance, UX)
|
||||||
|
- Optimized SEO (Server-Side Rendering)
|
||||||
|
- Clear separation of responsibilities (content vs code)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Resilience & Error Handling
|
||||||
|
|
||||||
|
With multiple external integrations, failures are inevitable. The solution was to implement **resilience patterns** using Polly library (.NET):
|
||||||
|
|
||||||
|
**Implemented patterns:**
|
||||||
|
|
||||||
|
**1. Retry**
|
||||||
|
- If Salesforce/Twilio/CRM API fail, system automatically retries 2-3x
|
||||||
|
- Wait grows exponentially (1s, 2s, 4s) to avoid overload
|
||||||
|
- Only transient errors (timeout, 503) are retried
|
||||||
|
|
||||||
|
**2. Circuit Breaker**
|
||||||
|
- If service fails 5x in a row, "opens circuit" for 30s
|
||||||
|
- During 30s, doesn't try anymore (avoids wasting resources)
|
||||||
|
- After 30s, tries again (may have recovered)
|
||||||
|
|
||||||
|
**3. Timeout**
|
||||||
|
- Each integration has maximum response time
|
||||||
|
- Avoids indefinitely stuck requests
|
||||||
|
|
||||||
|
**4. Fallback (Plan B)**
|
||||||
|
- Salesforce down: Order goes to queue, processes later
|
||||||
|
- Twilio down: Alert administrator via email
|
||||||
|
- CRM API down: Uses cache (24h old data)
|
||||||
|
- WordPress down: Displays pre-loaded static content
|
||||||
|
|
||||||
|
**Strategies per integration:**
|
||||||
|
|
||||||
|
| Integration | Retry | Circuit Breaker | Timeout | Plan B |
|
||||||
|
|----------|-------|-----------------|---------|----------|
|
||||||
|
| Salesforce | 3x (exponential) | 5 failures/30s | 10s | Retry queue |
|
||||||
|
| Twilio | 2x (linear) | 3 failures/60s | 5s | Admin alert |
|
||||||
|
| CRM API | 3x (exponential) | No | 15s | Cache |
|
||||||
|
| WordPress | No | No | 3s | Static content |
|
||||||
|
|
||||||
|
**Production result:**
|
||||||
|
- Salesforce had maintenance (1h) → System continued working (queue processed later)
|
||||||
|
- Twilio had instability → Automatic retry resolved 95% of cases
|
||||||
|
- Zero downtime perceived by users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overcoming Infrastructure Challenges
|
||||||
|
|
||||||
|
### Problem: Servers Not Provisioned
|
||||||
|
|
||||||
|
**Temporary solution:**
|
||||||
|
1. 100% local development (Docker Compose)
|
||||||
|
2. External service mocks (when credentials delayed)
|
||||||
|
3. CI/CD prepared but not active (awaiting infra)
|
||||||
|
|
||||||
|
**When servers arrived (week 6):**
|
||||||
|
- Deploy in 2 hours (already prepared)
|
||||||
|
- Zero surprises (everything tested locally)
|
||||||
|
- Smooth rollout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problem: Delayed Integration Credentials
|
||||||
|
|
||||||
|
**Impact:** Twilio and Salesforce took 3 weeks to be provisioned.
|
||||||
|
|
||||||
|
**Solution:** Create "mock" (simulated) versions of each integration:
|
||||||
|
- Twilio mock: Logs instead of sending real SMS
|
||||||
|
- Salesforce mock: Saves order to local JSON file
|
||||||
|
- CRM API mock: Returns fictional physician data
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
- Development environment: Uses mocks (no credentials needed)
|
||||||
|
- Production environment: Uses real integrations (when credentials arrive)
|
||||||
|
- Automatic switch based on configuration
|
||||||
|
|
||||||
|
**Result:** Team stayed 100% productive for 3 weeks, testing complete flows without depending on credentials.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problem: Team Inexperienced with Modern Stack
|
||||||
|
|
||||||
|
**Context:** Team had no experience with React, TypeScript, modern .NET Core, REST APIs.
|
||||||
|
|
||||||
|
**Enablement approach:**
|
||||||
|
|
||||||
|
**1. Pair Programming (1h/day per developer)**
|
||||||
|
- Tech lead works alongside dev
|
||||||
|
- Screen sharing + real-time explanation
|
||||||
|
- Dev writes code, tech lead guides
|
||||||
|
|
||||||
|
**2. Educational Code Review**
|
||||||
|
- Not just "approve" or "reject"
|
||||||
|
- Comments explain the **why** of each suggestion
|
||||||
|
- Example: "Always handle request errors! If API crashes, user needs to know what happened."
|
||||||
|
|
||||||
|
**3. Living Documentation**
|
||||||
|
- ADRs (Architecture Decision Records): Why did we choose X and not Y?
|
||||||
|
- READMEs: How to run, test, deploy
|
||||||
|
- Onboarding guide: From zero to first feature
|
||||||
|
|
||||||
|
**4. Weekly Live Coding (2h)**
|
||||||
|
- Tech lead solves real problem live
|
||||||
|
- Team observes thinking process
|
||||||
|
- Q&A at end
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
- After 4 weeks, team was autonomous
|
||||||
|
- Code quality consistently increased
|
||||||
|
- Devs started doing code review among themselves (peer review)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Successful Delivery Despite Challenges
|
||||||
|
|
||||||
|
**Context:** Program with multiple squads working in parallel.
|
||||||
|
|
||||||
|
**Achieved result:**
|
||||||
|
- **MVP delivered to production successfully**
|
||||||
|
- Controlled 2-month delay (significantly less than other program initiatives)
|
||||||
|
- All integrations working as planned
|
||||||
|
- Zero critical bugs in production (first week)
|
||||||
|
|
||||||
|
**Why was delivery successful?**
|
||||||
|
|
||||||
|
1. **Anticipated setup** - Git, processes, local Docker from day 1
|
||||||
|
2. **Strategic mocks** - Team wasn't blocked waiting for infra
|
||||||
|
3. **Solid architecture** - Resilience from the start
|
||||||
|
4. **Continuous upskilling** - Team learned by doing
|
||||||
|
5. **Proactive communication** - Risks reported early
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### MVP Metrics
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- Loading time: <2s (95th percentile)
|
||||||
|
- Lighthouse score: 95+ (mobile)
|
||||||
|
- SSL A+ rating
|
||||||
|
|
||||||
|
**Integrations:**
|
||||||
|
- Salesforce: 100% orders synchronized
|
||||||
|
- Twilio: 99.8% delivery rate
|
||||||
|
- CRM API: 10k validations/day (average)
|
||||||
|
- SQL Server: 50k records/month
|
||||||
|
|
||||||
|
**Adoption:**
|
||||||
|
- 2,000+ registered physicians (first 3 months)
|
||||||
|
- 15,000+ processed discount orders
|
||||||
|
- 4.8/5 satisfaction (internal survey)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Client Impact
|
||||||
|
|
||||||
|
**Digital transformation initiated:**
|
||||||
|
- Git implemented and adopted
|
||||||
|
- Established development processes
|
||||||
|
- Internal team enabled in modern stacks
|
||||||
|
- Cloud infrastructure configured (Azure)
|
||||||
|
- Evolution roadmap defined
|
||||||
|
|
||||||
|
**Foundation for future projects:**
|
||||||
|
- Architecture served as reference for other initiatives
|
||||||
|
- Documented code patterns (coding standards)
|
||||||
|
- Reused CI/CD pipelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`.NET 7` `C#` `Entity Framework Core` `SQL Server` `React 18` `Next.js 13` `TypeScript` `Tailwind CSS` `Salesforce API` `Twilio` `WordPress REST API` `Docker` `Polly` `OAuth2` `JWT` `LGPD Compliance`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Why Next.js instead of pure React?
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- Critical SEO (pharma needs to rank)
|
||||||
|
- Performance (physicians use mobile)
|
||||||
|
- Dynamic content (WordPress)
|
||||||
|
|
||||||
|
**Next.js offers:**
|
||||||
|
- SSR out-of-the-box
|
||||||
|
- API routes (BFF pattern)
|
||||||
|
- Automatic optimizations (image, fonts)
|
||||||
|
- Simplified deploy (Vercel, Azure)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why keep WordPress?
|
||||||
|
|
||||||
|
**Alternatives considered:**
|
||||||
|
1. Migrate content to database + custom CMS (time)
|
||||||
|
2. Strapi/Contentful (costs + learning curve)
|
||||||
|
3. **WordPress headless** (best trade-off)
|
||||||
|
|
||||||
|
**Advantages:**
|
||||||
|
- Marketing team already knows how to use
|
||||||
|
- Approved content was already there
|
||||||
|
- WordPress REST API is solid
|
||||||
|
- Zero cost (already running)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Why .NET 7 instead of Node.js?
|
||||||
|
|
||||||
|
**Context:** Client had preference for Microsoft stack.
|
||||||
|
|
||||||
|
**Additional benefits:**
|
||||||
|
- Superior performance (vs Node in APIs)
|
||||||
|
- Native type safety (C#)
|
||||||
|
- Entity Framework (mature ORM)
|
||||||
|
- Easy Azure integration (future deploy)
|
||||||
|
- Client team had familiarity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Infrastructure Delayed? Prepare Alternatives
|
||||||
|
|
||||||
|
Don't wait for servers/credentials to start:
|
||||||
|
- Local Docker is your friend
|
||||||
|
- Mocks allow progress
|
||||||
|
- CI/CD can be prepared before having where to deploy
|
||||||
|
|
||||||
|
**Lesson:** Control what you can control.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Processes > Tools
|
||||||
|
|
||||||
|
Even without corporate Git, I established:
|
||||||
|
- Branching strategy
|
||||||
|
- Code review
|
||||||
|
- Commit conventions
|
||||||
|
- Documentation standards
|
||||||
|
|
||||||
|
**Result:** When tools arrived, team already knew how to use them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Upskilling is Investment, Not Cost
|
||||||
|
|
||||||
|
Pair programming and code reviews took time, but:
|
||||||
|
- Team became autonomous faster
|
||||||
|
- Code quality increased
|
||||||
|
- Natural knowledge sharing
|
||||||
|
- Simplified onboarding of new devs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Resilience from the Start
|
||||||
|
|
||||||
|
Implementing Polly (retry, circuit breaker) at the start saved in production:
|
||||||
|
- Twilio had instability (resolved automatically)
|
||||||
|
- Salesforce had maintenance (queue worked)
|
||||||
|
- CRM API had slowness (cache mitigated)
|
||||||
|
|
||||||
|
**Lesson:** Don't leave resilience for "later". Failures will happen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Clear Risk Communication
|
||||||
|
|
||||||
|
I reported weekly:
|
||||||
|
- Blockers (infrastructure, credentials)
|
||||||
|
- Risks (deadlines, dependencies)
|
||||||
|
- Alternative solutions (mocks, workarounds)
|
||||||
|
|
||||||
|
**Result:** Stakeholders knew exact status and had no surprises.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenges & How They Were Overcome
|
||||||
|
|
||||||
|
| Challenge | Impact | Solution | Result |
|
||||||
|
|---------|---------|---------|-----------|
|
||||||
|
| No Git | Total blocker | Local setup + GitLab Cloud | Team productive day 1 |
|
||||||
|
| No servers | No dev environment | Local Docker Compose | Complete local dev/test |
|
||||||
|
| Delayed credentials | Integration blocked | Mock services | Progress without blocker |
|
||||||
|
| Inexperienced team | Low quality code | Pair prog + Code review | Ramp-up in 4 weeks |
|
||||||
|
| Multiple integrations | High complexity | Polly + patterns | Zero prod downtime |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Post-MVP)
|
||||||
|
|
||||||
|
**Roadmap suggested to client:**
|
||||||
|
|
||||||
|
1. **Phase 2: Feature expansion**
|
||||||
|
- Dashboard for physicians (order history)
|
||||||
|
- Push notifications (Firebase)
|
||||||
|
- E-commerce integration (direct purchase)
|
||||||
|
|
||||||
|
2. **Phase 3: Optimizations**
|
||||||
|
- Distributed cache (Redis)
|
||||||
|
- CDN for static assets
|
||||||
|
- Advanced analytics (Amplitude)
|
||||||
|
|
||||||
|
3. **Phase 4: Scale**
|
||||||
|
- Kubernetes (AKS)
|
||||||
|
- Microservices (break monolith)
|
||||||
|
- Event-driven architecture (Azure Service Bus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Result:** MVP delivered to production despite starting literally from zero, establishing solid foundations for client's digital transformation.
|
||||||
|
|
||||||
|
[Need to build an MVP in a challenging scenario? Get in touch](#contact)
|
||||||
211
Content/Cases/en/sap-integration-healthcare.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
title: "SAP Healthcare Integration System"
|
||||||
|
slug: "sap-integration-healthcare"
|
||||||
|
summary: "Bidirectional integration processing 100k+ transactions/day with 99.9% uptime"
|
||||||
|
client: "Confidential - Healthcare Multinational"
|
||||||
|
industry: "Healthcare"
|
||||||
|
timeline: "6 months"
|
||||||
|
role: "Integration Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- SAP
|
||||||
|
- C#
|
||||||
|
- .NET
|
||||||
|
- Integrations
|
||||||
|
- Enterprise
|
||||||
|
- Healthcare
|
||||||
|
featured: true
|
||||||
|
order: 1
|
||||||
|
date: 2023-06-15
|
||||||
|
seo_title: "Case Study: SAP Healthcare Integration - 100k Transactions/Day"
|
||||||
|
seo_description: "How we architected SAP integration system processing 100k+ daily transactions with 99.9% uptime for healthcare company."
|
||||||
|
seo_keywords: "SAP integration, C#, .NET, SAP Connector, enterprise integration, healthcare"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**Client:** Healthcare Multinational (confidential)
|
||||||
|
**Size:** 100,000+ employees
|
||||||
|
**Project:** Benefits integration
|
||||||
|
**Timeline:** 6 months
|
||||||
|
**My Role:** Integration Architect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
Client had internal benefits management system that needed to sync with SAP ECC to process payroll.
|
||||||
|
|
||||||
|
### Main pain points:
|
||||||
|
- Manual process prone to errors
|
||||||
|
- 3-5 day delay between systems
|
||||||
|
- 100k employees waiting for processing
|
||||||
|
- Load spikes (month-end)
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
- Limited budget (no SAP BTP)
|
||||||
|
- Small internal SAP team (2 developers)
|
||||||
|
- Tight deadline (6-month go-live)
|
||||||
|
- Legacy .NET Framework 4.5 system
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Bidirectional integration architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Internal System] ←→ [Queue] ←→ [SAP Connector] ←→ [SAP ECC]
|
||||||
|
↓ ↓
|
||||||
|
[MongoDB Logs] [ABAP Z_BENEFITS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components:
|
||||||
|
- .NET Service with SAP Connector (NCo 3.0)
|
||||||
|
- Custom ABAP transaction (Z_BENEFITS)
|
||||||
|
- Queue system (RabbitMQ) for retry logic
|
||||||
|
- MongoDB for audit and troubleshooting
|
||||||
|
- Scheduler (Hangfire) for batch processing
|
||||||
|
|
||||||
|
### Flow:
|
||||||
|
1. System generates changes (new hires, modifications)
|
||||||
|
2. Service processes batch (500 records/batch)
|
||||||
|
3. SAP Connector calls Z_BENEFITS via RFC
|
||||||
|
4. SAP returns status (success/error)
|
||||||
|
5. Automatic retry if failure (max 3x)
|
||||||
|
6. MongoDB logs for troubleshooting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
### Metrics:
|
||||||
|
- **100k+** transactions/day processed
|
||||||
|
- **99.9%** uptime
|
||||||
|
- Reduced **5 days → 4 hours** (delay)
|
||||||
|
- **80%** reduction in processing time
|
||||||
|
- **Zero** manual errors (vs 2-3% before)
|
||||||
|
|
||||||
|
### Benefits:
|
||||||
|
- Employees receive benefits on-time
|
||||||
|
- HR team saves 40h/month (manual work)
|
||||||
|
- Complete audit (compliance)
|
||||||
|
- Scalable (30% year-over-year growth without refactor)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`C#` `.NET Framework 4.5` `SAP NCo 3.0` `RabbitMQ` `MongoDB` `Hangfire` `Docker` `SAP ECC` `ABAP` `RFC`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisions & Motivation
|
||||||
|
|
||||||
|
### Decision 1: SAP Connector vs SAP BTP
|
||||||
|
|
||||||
|
**Options evaluated:**
|
||||||
|
- SAP BTP (events, modern APIs, cloud)
|
||||||
|
- SAP Connector (direct RFC, on-premise)
|
||||||
|
|
||||||
|
**We chose:** SAP Connector
|
||||||
|
|
||||||
|
**Motivation:**
|
||||||
|
- Client had on-premise SAP ECC (not S/4)
|
||||||
|
- Budget didn't allow BTP license
|
||||||
|
- SAP team comfortable with ABAP/RFC
|
||||||
|
- Needs met with RFC (didn't need real-time event-driven)
|
||||||
|
|
||||||
|
**Accepted trade-off:**
|
||||||
|
- Less "modern" than BTP, but 100% functional
|
||||||
|
- $0 additional cost vs $30k+/year BTP
|
||||||
|
- 2 months faster delivery (no BTP learning curve)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Decision 2: Queue System vs Direct Calls
|
||||||
|
|
||||||
|
**Options evaluated:**
|
||||||
|
- Direct synchronous calls (simpler)
|
||||||
|
- Queue with retry (more complex)
|
||||||
|
|
||||||
|
**We chose:** Queue + Retry
|
||||||
|
|
||||||
|
**Motivation:**
|
||||||
|
- SAP occasionally unavailable (maintenance)
|
||||||
|
- Load spikes (month-end = 200k requests)
|
||||||
|
- Ensure zero data loss
|
||||||
|
- Resilience > simplicity (critical environment)
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- RabbitMQ with dead-letter queue
|
||||||
|
- Exponential retry (1min, 5min, 15min)
|
||||||
|
- Alerts if 3 consecutive failures
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
- Zero data loss in 2 years production
|
||||||
|
- HR team doesn't need to "keep watch"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Decision 3: Custom ABAP vs Standard
|
||||||
|
|
||||||
|
**Options evaluated:**
|
||||||
|
- Standard SAP BAPIs (zero ABAP code)
|
||||||
|
- Custom transaction (Z_BENEFITS)
|
||||||
|
|
||||||
|
**We chose:** Custom transaction
|
||||||
|
|
||||||
|
**Motivation:**
|
||||||
|
- Standard BAPIs didn't have business-specific validations
|
||||||
|
- Client wanted logic centralized in SAP (single source of truth)
|
||||||
|
- Allowed complex validations (eligibility, dependents, limits)
|
||||||
|
|
||||||
|
**Trade-off:**
|
||||||
|
- Requires ABAP maintenance (internal SAP team)
|
||||||
|
- But: Client preferred vs duplicate logic (risk of desync)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Alternatives NOT Chosen
|
||||||
|
|
||||||
|
**Webhook/Callback (Event-Driven):**
|
||||||
|
- Client had no infrastructure to expose APIs
|
||||||
|
- Internal system behind firewall
|
||||||
|
- Batch polling works well for the case
|
||||||
|
|
||||||
|
**Kubernetes Microservices:**
|
||||||
|
- Overkill for single integration
|
||||||
|
- Team had no K8s expertise
|
||||||
|
- Simple Docker sufficient
|
||||||
|
|
||||||
|
**Real-time Sync (<1min):**
|
||||||
|
- Business doesn't need (daily batch ok)
|
||||||
|
- Infrastructure cost would increase 3x
|
||||||
|
- 4h delay acceptable for payroll
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Learnings
|
||||||
|
|
||||||
|
### What worked very well:
|
||||||
|
- Involve SAP team from day 1 (buy-in)
|
||||||
|
- MongoDB for logs (10x faster troubleshooting)
|
||||||
|
- Retry logic saved countless times
|
||||||
|
|
||||||
|
### What I would do differently:
|
||||||
|
- Add health check endpoint earlier
|
||||||
|
- Monitoring dashboard from start (added later)
|
||||||
|
|
||||||
|
### Lessons for next projects:
|
||||||
|
- Client "limited budget" ≠ "limited solution" - creativity solves
|
||||||
|
- Document ALL architectural decisions (team turnover)
|
||||||
|
- Simplicity beats complexity when both work (KISS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Need Something Similar?
|
||||||
|
|
||||||
|
Complex SAP integrations, legacy systems, or high-availability architecture?
|
||||||
|
|
||||||
|
[Let's talk about your challenge →](/#contact)
|
||||||
329
Content/Cases/es/asp-to-dotnet-migration.md
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
---
|
||||||
|
title: "Migración ASP 3.0 a .NET Core - Sistema de Rastreo de Cargas"
|
||||||
|
slug: "asp-to-dotnet-migration"
|
||||||
|
summary: "Tech Lead en la migración gradual de sistema crítico ASP 3.0 a .NET Core, con sincronización de datos entre versiones y reducción de costos de $20k/año en APIs de mapeo."
|
||||||
|
client: "Empresa de Logística y Rastreo"
|
||||||
|
industry: "Logística & Seguridad"
|
||||||
|
timeline: "12 meses (migración completa)"
|
||||||
|
role: "Tech Lead & Solution Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- ASP Classic
|
||||||
|
- .NET Core
|
||||||
|
- SQL Server
|
||||||
|
- Migration
|
||||||
|
- Tech Lead
|
||||||
|
- OSRM
|
||||||
|
- APIs
|
||||||
|
- Arquitectura
|
||||||
|
featured: true
|
||||||
|
order: 2
|
||||||
|
date: 2015-06-01
|
||||||
|
seo_title: "Migración ASP 3.0 a .NET Core - Case Carneiro Tech"
|
||||||
|
seo_description: "Caso de migración gradual de aplicación ASP 3.0 a .NET Core con sincronización de datos y reducción de $20k/año en costos de APIs."
|
||||||
|
seo_keywords: "ASP migration, .NET Core, legacy modernization, SQL Server, OSRM, tech lead, routing API"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
Sistema crítico de monitoreo de cargas de alto valor (TVs LED de $600 cada una, cargamentos de hasta 1000 unidades) utilizando rastreo GPS vía satélite. La aplicación cubría todo el ciclo: desde registro y evaluación de conductores (verificación de antecedentes policiales) hasta monitoreo en tiempo real y entrega final.
|
||||||
|
|
||||||
|
**Desafío principal:** Migrar aplicación legacy ASP 3.0 a .NET Core sin downtime, manteniendo operación crítica 24/7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafío
|
||||||
|
|
||||||
|
### Sistema Legacy Crítico
|
||||||
|
|
||||||
|
La empresa operaba un sistema mission-critical en **ASP 3.0** (Classic ASP) que no podía detenerse:
|
||||||
|
|
||||||
|
**Tecnología legacy:**
|
||||||
|
- ASP 3.0 (tecnología de 1998)
|
||||||
|
- SQL Server 2005
|
||||||
|
- Cluster failover on-premises (perfectamente capaz de soportar la carga)
|
||||||
|
- Integración con rastreadores GPS vía satélite
|
||||||
|
- Google Maps API (costo: **$20,000/año** solo para cálculo de rutas)
|
||||||
|
|
||||||
|
**Restricciones:**
|
||||||
|
- Sistema operando 24/7 con cargas de alto valor
|
||||||
|
- Imposibilidad de downtime durante migración
|
||||||
|
- Múltiples módulos interdependientes
|
||||||
|
- Equipo necesitaba continuar desarrollando features durante la migración
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitectura de Solución
|
||||||
|
|
||||||
|
### Fase 1: Preparación de Infraestructura (Meses 1-3)
|
||||||
|
|
||||||
|
#### Upgrade de Base de Datos
|
||||||
|
```
|
||||||
|
SQL Server 2005 → SQL Server 2014
|
||||||
|
- Backup completo y validación
|
||||||
|
- Migración de stored procedures
|
||||||
|
- Optimización de índices
|
||||||
|
- Pruebas de performance
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Estrategia de Sincronización Dual-Write
|
||||||
|
|
||||||
|
Implementé un **sistema de sincronización bidireccional** que permitía:
|
||||||
|
|
||||||
|
1. **Módulos nuevos (.NET Core)** escribían en la base de datos nueva
|
||||||
|
2. **Trigger automático** sincronizaba datos hacia la base de datos legacy
|
||||||
|
3. **Módulos antiguos (ASP 3.0)** continuaban funcionando normalmente
|
||||||
|
4. **Zero downtime** durante toda la migración
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Ejemplo de sincronización implementada
|
||||||
|
public class DualWriteService
|
||||||
|
{
|
||||||
|
public async Task SaveDriver(Driver driver)
|
||||||
|
{
|
||||||
|
// Escribe en base de datos nueva (.NET Core)
|
||||||
|
await _newDbContext.Drivers.AddAsync(driver);
|
||||||
|
await _newDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Trigger SQL sincroniza automáticamente hacia base de datos legacy
|
||||||
|
// Módulos ASP 3.0 continúan funcionando
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**¿Por qué este enfoque?**
|
||||||
|
- Permitió migración **módulo por módulo**
|
||||||
|
- Equipo podía continuar desarrollando
|
||||||
|
- Rollback sencillo si fuera necesario
|
||||||
|
- Reducción de riesgo operacional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Migración Gradual de Módulos (Meses 4-12)
|
||||||
|
|
||||||
|
Migré los módulos en orden de complejidad creciente:
|
||||||
|
|
||||||
|
**Orden de migración:**
|
||||||
|
1. ✅ Registros básicos (conductores, vehículos)
|
||||||
|
2. ✅ Evaluación de riesgo (integración con base policial)
|
||||||
|
3. ✅ Gestión de cargas y rutas
|
||||||
|
4. ✅ Monitoreo GPS en tiempo real
|
||||||
|
5. ✅ Alertas y notificaciones
|
||||||
|
6. ✅ Reportes y analytics
|
||||||
|
|
||||||
|
**Stack de la aplicación migrada:**
|
||||||
|
- `.NET Core 1.0` (2015-2016 era el inicio de .NET Core)
|
||||||
|
- `Entity Framework Core`
|
||||||
|
- `SignalR` para monitoreo real-time
|
||||||
|
- `SQL Server 2014`
|
||||||
|
- APIs RESTful
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Reducción de Costos con OSRM (Ahorro de $20k/año)
|
||||||
|
|
||||||
|
#### Problema: Costo Prohibitivo de Google Maps
|
||||||
|
|
||||||
|
La empresa gastaba **$20,000/año** solo en Google Maps Directions API para cálculo de rutas de camiones.
|
||||||
|
|
||||||
|
#### Solución: OSRM (Open Source Routing Machine)
|
||||||
|
|
||||||
|
Implementé una solución basada en **OSRM** (motor de ruteo open-source):
|
||||||
|
|
||||||
|
**Arquitectura de la solución:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Frontend │
|
||||||
|
│ (Leaflet.js) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ┌──────────────┐
|
||||||
|
│ API Wrapper │─────▶│ OSRM Server │
|
||||||
|
│ (.NET Core) │ │ (self-hosted)│
|
||||||
|
└────────┬────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Google Maps │
|
||||||
|
│ (display only) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementación:**
|
||||||
|
|
||||||
|
1. **Servidor OSRM configurado** en servidor propio
|
||||||
|
2. **API wrapper amigable** en .NET Core que:
|
||||||
|
- Recibía origen/destino
|
||||||
|
- Consultaba OSRM (gratuito)
|
||||||
|
- Devolvía todos los puntos de la ruta
|
||||||
|
- Formateaba para el frontend
|
||||||
|
3. **Frontend** dibujaba la ruta en Google Maps (solo visualización, sin API de rutas)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("route")]
|
||||||
|
public async Task<IActionResult> GetRoute(double originLat, double originLng,
|
||||||
|
double destLat, double destLng)
|
||||||
|
{
|
||||||
|
// Consulta OSRM (gratuito)
|
||||||
|
var osrmResponse = await _osrmClient.GetRouteAsync(
|
||||||
|
originLat, originLng, destLat, destLng);
|
||||||
|
|
||||||
|
// Retorna puntos formateados para el frontend
|
||||||
|
return Ok(new {
|
||||||
|
points = osrmResponse.Routes[0].Geometry.Coordinates,
|
||||||
|
distance = osrmResponse.Routes[0].Distance,
|
||||||
|
duration = osrmResponse.Routes[0].Duration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend con Leaflet:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Dibuja ruta en el mapa (Google Maps solo para tiles)
|
||||||
|
L.polyline(routePoints, {color: 'red'}).addTo(map);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Intento con OpenStreetMap
|
||||||
|
|
||||||
|
Intenté sustituir también Google Maps (tiles) por **OpenStreetMap**, que funcionó técnicamente, pero:
|
||||||
|
|
||||||
|
❌ **A los usuarios no les gustó** la apariencia
|
||||||
|
❌ Preferían la interfaz familiar de Google Maps
|
||||||
|
|
||||||
|
✅ **Decisión:** Mantener Google Maps solo para visualización (sin costo de API de rutas)
|
||||||
|
|
||||||
|
**Resultado:** Ahorro de **~$20,000/año** manteniendo calidad de las rutas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultados e Impacto
|
||||||
|
|
||||||
|
### Migración Completa en 12 Meses
|
||||||
|
|
||||||
|
✅ **100% de los módulos** migrados de ASP 3.0 a .NET Core
|
||||||
|
✅ **Zero downtime** durante toda la migración
|
||||||
|
✅ **Equipo productivo** durante todo el proceso
|
||||||
|
✅ Sistema más rápido y escalable
|
||||||
|
|
||||||
|
### Reducción de Costos
|
||||||
|
|
||||||
|
💰 **$20,000/año ahorrados** con sustitución de Google Maps Directions API
|
||||||
|
📉 **Infraestructura optimizada** con SQL Server 2014
|
||||||
|
|
||||||
|
### Mejoras Técnicas
|
||||||
|
|
||||||
|
🚀 **Performance:** Aplicación .NET Core 3x más rápida que ASP 3.0
|
||||||
|
🔒 **Seguridad:** Stack moderno con parches de seguridad activos
|
||||||
|
🛠️ **Mantenibilidad:** Código C# moderno vs VBScript legacy
|
||||||
|
📊 **Monitoreo:** SignalR para tracking real-time más eficiente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fase No Ejecutada: Microservicios & Cloud
|
||||||
|
|
||||||
|
### Planificación Inicial
|
||||||
|
|
||||||
|
Participé en el **diseño y concepción** de la segunda fase (nunca ejecutada):
|
||||||
|
|
||||||
|
**Arquitectura planificada:**
|
||||||
|
- Migración a **Azure** (cloud estaba apenas comenzando en 2015)
|
||||||
|
- División en **microservicios**:
|
||||||
|
- Servicio de autenticación
|
||||||
|
- Servicio de GPS/tracking
|
||||||
|
- Servicio de rutas
|
||||||
|
- Servicio de notificaciones
|
||||||
|
- **Event-driven architecture** con message queues
|
||||||
|
|
||||||
|
**Por qué no fue ejecutada:**
|
||||||
|
|
||||||
|
Salí de la empresa inmediatamente después de concluir la migración a .NET Core. La segunda fase quedó planificada pero no fue implementada por mí.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`ASP 3.0` `VBScript` `.NET Core 1.0` `C#` `Entity Framework Core` `SQL Server 2005` `SQL Server 2014` `OSRM` `Leaflet.js` `Google Maps` `SignalR` `REST APIs` `GPS/Satellite` `Migration Strategy` `Dual-Write Pattern`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisiones Clave & Trade-offs
|
||||||
|
|
||||||
|
### ¿Por qué sincronización dual-write?
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
1. ❌ Big Bang migration (demasiado arriesgado)
|
||||||
|
2. ❌ Mantener todo en ASP 3.0 (insostenible)
|
||||||
|
3. ✅ **Migración gradual con sync** (elegido)
|
||||||
|
|
||||||
|
**Justificación:**
|
||||||
|
- Sistema crítico no podía detenerse
|
||||||
|
- Permitió rollback módulo por módulo
|
||||||
|
- Equipo continuó productivo
|
||||||
|
|
||||||
|
### ¿Por qué OSRM en vez de otros?
|
||||||
|
|
||||||
|
**Alternativas:**
|
||||||
|
- Google Maps: $20k/año ❌
|
||||||
|
- Mapbox: Licencia paga ❌
|
||||||
|
- GraphHopper: Configuración compleja ❌
|
||||||
|
- **OSRM: Open-source, rápido, configurable** ✅
|
||||||
|
|
||||||
|
### ¿Por qué no OpenStreetMap para tiles?
|
||||||
|
|
||||||
|
**Decisión basada en UX:**
|
||||||
|
- Técnicamente funcionó perfectamente
|
||||||
|
- Usuarios preferían interfaz familiar de Google
|
||||||
|
- **Compromiso:** Google Maps para visualización (gratis) + OSRM para rutas (gratis)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecciones Aprendidas
|
||||||
|
|
||||||
|
### 1. Migración Gradual > Big Bang
|
||||||
|
|
||||||
|
Migrar módulo por módulo con sincronización permitió:
|
||||||
|
- Aprendizaje continuo
|
||||||
|
- Ajustes de ruta durante el proceso
|
||||||
|
- Confianza del equipo y stakeholders
|
||||||
|
|
||||||
|
### 2. Open Source Puede Ahorrar Mucho
|
||||||
|
|
||||||
|
OSRM ahorró **$20k/año** sin pérdida de calidad. Pero requiere:
|
||||||
|
- Expertise para configurar
|
||||||
|
- Infraestructura propia
|
||||||
|
- Mantenimiento continuo
|
||||||
|
|
||||||
|
### 3. UX > Tecnología A Veces
|
||||||
|
|
||||||
|
OpenStreetMap era técnicamente superior (gratuito), pero usuarios prefirieron Google Maps. **Lección:** Escuchar a los usuarios finales.
|
||||||
|
|
||||||
|
### 4. Planifique Cloud, pero Valide el ROI
|
||||||
|
|
||||||
|
En 2015, cloud estaba comenzando. La infraestructura on-premises (cluster SQL Server) era perfectamente capaz. **No fuerce cloud si no hay beneficio claro.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contexto: Por qué 2015 fue un Momento Especial
|
||||||
|
|
||||||
|
**Estado de la tecnología en 2015:**
|
||||||
|
|
||||||
|
- ☁️ **Cloud en pañales:** AWS existía, Azure creciendo, pero adopción corporativa aún baja
|
||||||
|
- 🆕 **.NET Core 1.0 lanzado** en junio/2016 (usamos RC durante proyecto)
|
||||||
|
- 📱 **Microservicios:** Concepto nuevo, Docker en adopción inicial
|
||||||
|
- 🗺️ **Google Maps dominante:** APIs pagas, pocas alternativas open-source maduras
|
||||||
|
|
||||||
|
**Desafíos de la época:**
|
||||||
|
- Herramientas de migración ASP→.NET inexistentes
|
||||||
|
- Documentación .NET Core escasa (versión 1.0!)
|
||||||
|
- Patrones de arquitectura aún consolidándose
|
||||||
|
|
||||||
|
Este proyecto fue **pionero** al adoptar .NET Core al inicio, cuando la mayoría migraba a .NET Framework 4.x.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** Migración exitosa de sistema crítico 24/7, ahorro de $20k/año, y base sólida para evolución futura.
|
||||||
|
|
||||||
|
[¿Quiere discutir una migración similar? Póngase en contacto](#contact)
|
||||||
382
Content/Cases/es/cnpj-fast-process.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
---
|
||||||
|
title: "CNPJ Fast - Proceso de Migración a CNPJ Alfanumérico"
|
||||||
|
slug: "cnpj-fast-process"
|
||||||
|
summary: "Creación de metodología estructurada para migración de aplicaciones al nuevo formato de CNPJ alfanumérico brasileño, vendida a aseguradora y empresa de cobranza."
|
||||||
|
client: "Empresa de Consultoría (Interno)"
|
||||||
|
industry: "Consultoría & Transformación Digital"
|
||||||
|
timeline: "3 meses (creación del proceso)"
|
||||||
|
role: "Solution Architect & Process Designer"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- Process Design
|
||||||
|
- CNPJ
|
||||||
|
- Migration Strategy
|
||||||
|
- Regulatory Compliance
|
||||||
|
- Consulting
|
||||||
|
- Sales Enablement
|
||||||
|
featured: true
|
||||||
|
order: 3
|
||||||
|
date: 2024-09-01
|
||||||
|
seo_title: "CNPJ Fast - Metodología de Migración CNPJ Alfanumérico"
|
||||||
|
seo_description: "Caso de creación de proceso estructurado para migración a CNPJ alfanumérico brasileño, vendido a aseguradora y empresa de cobranza."
|
||||||
|
seo_keywords: "CNPJ alfanumérico, migration process, regulatory compliance, consulting, methodology"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
Con la introducción del **CNPJ alfanumérico** por la Receita Federal brasileña, las empresas enfrentaban el desafío de adaptar sus aplicaciones legacy que almacenaban CNPJ como campos numéricos (`bigint`, `numeric`, `int`).
|
||||||
|
|
||||||
|
Creé **CNPJ Fast**, una metodología estructurada para evaluar, planificar y ejecutar migraciones de CNPJ en aplicaciones y bases de datos corporativas.
|
||||||
|
|
||||||
|
**Resultado:** Proceso vendido a **2 clientes** (aseguradora y empresa de cobranza) antes incluso de la implementación.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafío
|
||||||
|
|
||||||
|
### Cambio Regulatorio Complejo
|
||||||
|
|
||||||
|
**Contexto regulatorio:**
|
||||||
|
- Receita Federal brasileña introdujo **CNPJ alfanumérico**
|
||||||
|
- CNPJ deja de ser solo números (14 dígitos)
|
||||||
|
- Pasa a aceptar **letras y números** (formato alfanumérico)
|
||||||
|
|
||||||
|
**Impacto en las empresas:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ANTES: CNPJ numérico
|
||||||
|
CNPJ BIGINT -- 12345678000190
|
||||||
|
|
||||||
|
-- DESPUÉS: CNPJ alfanumérico
|
||||||
|
CNPJ VARCHAR(18) -- 12.ABC.678/0001-90
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problemas identificados:**
|
||||||
|
|
||||||
|
1. 🗄️ **Base de datos:** Columnas `BIGINT`, `NUMERIC`, `INT` no soportan caracteres
|
||||||
|
2. 🔑 **Claves primarias:** CNPJ usado como PK en varias tablas
|
||||||
|
3. 🔗 **Foreign keys:** Relaciones entre tablas
|
||||||
|
4. 📊 **Volumen:** Millones de registros para migrar
|
||||||
|
5. 💻 **Aplicaciones:** Validaciones, máscaras, reglas de negocio
|
||||||
|
6. 🧪 **Pruebas:** Garantizar integridad después de migración
|
||||||
|
7. ⏱️ **Downtime:** Ventanas de mantenimiento limitadas
|
||||||
|
|
||||||
|
**Sin un proceso estructurado**, empresas arriesgaban:
|
||||||
|
- Pérdida de datos
|
||||||
|
- Inconsistencias en la base de datos
|
||||||
|
- Aplicaciones rotas
|
||||||
|
- Downtime prolongado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solución: CNPJ Fast Process
|
||||||
|
|
||||||
|
### Metodología en 5 Fases
|
||||||
|
|
||||||
|
Diseñé un proceso estructurado que podría ser replicado en diferentes clientes:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 1: DISCOVERY & ASSESSMENT │
|
||||||
|
│ - Inventario de aplicaciones │
|
||||||
|
│ - Análisis de schemas de base de datos │
|
||||||
|
│ - Identificación de tablas impactadas │
|
||||||
|
│ - Estimación de volumen de datos │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 2: IMPACT ANALYSIS │
|
||||||
|
│ - Mapeo de dependencias │
|
||||||
|
│ - Análisis de claves primarias/foráneas │
|
||||||
|
│ - Identificación de reglas de negocio │
|
||||||
|
│ - Evaluación de riesgo │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 3: MIGRATION PLANNING │
|
||||||
|
│ - Estrategia de migración (phased commits) │
|
||||||
|
│ - Scripts SQL automatizados │
|
||||||
|
│ - Plan de rollback │
|
||||||
|
│ - Ventanas de mantenimiento │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 4: EXECUTION │
|
||||||
|
│ - Migración de datos en lotes │
|
||||||
|
│ - Actualización de aplicaciones │
|
||||||
|
│ - Pruebas de integración │
|
||||||
|
│ - Validación de integridad │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 5: VALIDATION & GO-LIVE │
|
||||||
|
│ - Pruebas de regresión │
|
||||||
|
│ - Validación de performance │
|
||||||
|
│ - Go-live coordinado │
|
||||||
|
│ - Monitoreo post-migración │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 1: Discovery & Assessment
|
||||||
|
|
||||||
|
**Objetivo:** Entender el alcance completo de la migración
|
||||||
|
|
||||||
|
**Entregables:**
|
||||||
|
|
||||||
|
1. **Inventario de Aplicaciones**
|
||||||
|
- Lista de aplicaciones que usan CNPJ
|
||||||
|
- Tecnologías (ASP 3.0, VB6, .NET, microservicios)
|
||||||
|
- Criticidad de cada aplicación
|
||||||
|
|
||||||
|
2. **Análisis de Schema**
|
||||||
|
```sql
|
||||||
|
-- Script de descubrimiento automático
|
||||||
|
SELECT
|
||||||
|
t.TABLE_SCHEMA,
|
||||||
|
t.TABLE_NAME,
|
||||||
|
c.COLUMN_NAME,
|
||||||
|
c.DATA_TYPE,
|
||||||
|
c.CHARACTER_MAXIMUM_LENGTH
|
||||||
|
FROM INFORMATION_SCHEMA.TABLES t
|
||||||
|
JOIN INFORMATION_SCHEMA.COLUMNS c
|
||||||
|
ON t.TABLE_NAME = c.TABLE_NAME
|
||||||
|
WHERE c.COLUMN_NAME LIKE '%CNPJ%'
|
||||||
|
AND c.DATA_TYPE IN ('bigint', 'numeric', 'int')
|
||||||
|
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Estimación de Volumen**
|
||||||
|
- Total de registros por tabla
|
||||||
|
- Tamaño en GB
|
||||||
|
- Tiempo estimado de migración
|
||||||
|
|
||||||
|
**Ejemplo de output:**
|
||||||
|
|
||||||
|
| Tabla | Columna | Tipo Actual | Registros | Criticidad |
|
||||||
|
|--------|--------|------------|-----------|-------------|
|
||||||
|
| Clientes | CNPJ_Cliente | BIGINT | 8.000.000 | Alta |
|
||||||
|
| Proveedores | CNPJ_Proveedor | NUMERIC(14) | 2.500.000 | Media |
|
||||||
|
| Transacciones | CNPJ_Pagador | BIGINT | 90.000.000 | Crítica |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Impact Analysis
|
||||||
|
|
||||||
|
**Objetivo:** Mapear todas las dependencias y riesgos
|
||||||
|
|
||||||
|
**Análisis de claves:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Identifica PKs y FKs que involucran CNPJ
|
||||||
|
SELECT
|
||||||
|
fk.name AS FK_Name,
|
||||||
|
tp.name AS Parent_Table,
|
||||||
|
cp.name AS Parent_Column,
|
||||||
|
tr.name AS Referenced_Table,
|
||||||
|
cr.name AS Referenced_Column
|
||||||
|
FROM sys.foreign_keys fk
|
||||||
|
INNER JOIN sys.tables tp ON fk.parent_object_id = tp.object_id
|
||||||
|
INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
||||||
|
INNER JOIN sys.columns cp ON fkc.parent_column_id = cp.column_id
|
||||||
|
AND fkc.parent_object_id = cp.object_id
|
||||||
|
INNER JOIN sys.tables tr ON fk.referenced_object_id = tr.object_id
|
||||||
|
INNER JOIN sys.columns cr ON fkc.referenced_column_id = cr.column_id
|
||||||
|
AND fkc.referenced_object_id = cr.object_id
|
||||||
|
WHERE cp.name LIKE '%CNPJ%' OR cr.name LIKE '%CNPJ%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Evaluación de Riesgo:**
|
||||||
|
|
||||||
|
- 🔴 **Alto:** Tablas con CNPJ como PK y >10M registros
|
||||||
|
- 🟡 **Medio:** Tablas con FK hacia CNPJ
|
||||||
|
- 🟢 **Bajo:** Tablas sin constraints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Migration Planning
|
||||||
|
|
||||||
|
**Estrategia de migración gradual:**
|
||||||
|
|
||||||
|
Para evitar bloqueo de base de datos, diseñé estrategia de **phased commits**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Estrategia para tablas grandes (>1M registros)
|
||||||
|
|
||||||
|
-- 1. Agregar nueva columna VARCHAR
|
||||||
|
ALTER TABLE Clientes
|
||||||
|
ADD CNPJ_Cliente_New VARCHAR(18) NULL;
|
||||||
|
|
||||||
|
-- 2. Migración en lotes (commits faseados)
|
||||||
|
DECLARE @BatchSize INT = 100000;
|
||||||
|
DECLARE @RowsAffected INT = 1;
|
||||||
|
|
||||||
|
WHILE @RowsAffected > 0
|
||||||
|
BEGIN
|
||||||
|
UPDATE TOP (@BatchSize) Clientes
|
||||||
|
SET CNPJ_Cliente_New = FORMAT(CNPJ_Cliente, '00000000000000')
|
||||||
|
WHERE CNPJ_Cliente_New IS NULL;
|
||||||
|
|
||||||
|
SET @RowsAffected = @@ROWCOUNT;
|
||||||
|
WAITFOR DELAY '00:00:01'; -- Pausa entre lotes
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 3. Remover constraints (PKs, FKs)
|
||||||
|
ALTER TABLE Clientes DROP CONSTRAINT PK_Clientes;
|
||||||
|
|
||||||
|
-- 4. Renombrar columnas
|
||||||
|
EXEC sp_rename 'Clientes.CNPJ_Cliente', 'CNPJ_Cliente_Old', 'COLUMN';
|
||||||
|
EXEC sp_rename 'Clientes.CNPJ_Cliente_New', 'CNPJ_Cliente', 'COLUMN';
|
||||||
|
|
||||||
|
-- 5. Recrear constraints
|
||||||
|
ALTER TABLE Clientes
|
||||||
|
ADD CONSTRAINT PK_Clientes PRIMARY KEY (CNPJ_Cliente);
|
||||||
|
|
||||||
|
-- 6. Remover columna antigua (tras validación)
|
||||||
|
ALTER TABLE Clientes DROP COLUMN CNPJ_Cliente_Old;
|
||||||
|
```
|
||||||
|
|
||||||
|
**¿Por qué este enfoque?**
|
||||||
|
|
||||||
|
- ✅ Evita lock de tabla entera
|
||||||
|
- ✅ Permite pausar/reanudar migración
|
||||||
|
- ✅ Minimiza impacto en producción
|
||||||
|
- ✅ Facilita rollback si es necesario
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4 & 5: Execution y Validation
|
||||||
|
|
||||||
|
**Checklist de ejecución:**
|
||||||
|
|
||||||
|
- [ ] Backup completo de la base de datos
|
||||||
|
- [ ] Ejecutar scripts de migración en lotes
|
||||||
|
- [ ] Actualizar aplicaciones (validaciones, máscaras)
|
||||||
|
- [ ] Pruebas de integración
|
||||||
|
- [ ] Validación de integridad referencial
|
||||||
|
- [ ] Pruebas de performance
|
||||||
|
- [ ] Go-live coordinado
|
||||||
|
- [ ] Monitoreo 24h post-migración
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sales Enablement: Presentación UX
|
||||||
|
|
||||||
|
**Colaboración con Gestor de UX:**
|
||||||
|
|
||||||
|
El gestor de UX de la empresa creó una **presentación visual impactante** del proceso CNPJ Fast:
|
||||||
|
|
||||||
|
**Contenido de la presentación:**
|
||||||
|
- 📊 Infografías del proceso de 5 fases
|
||||||
|
- 📈 Ejemplos de estimaciones de tiempo/costo
|
||||||
|
- 🎯 Casos de uso (aseguradoras, bancos, fintechs)
|
||||||
|
- ✅ Checklist ejecutivo
|
||||||
|
- 📋 Templates de documentación
|
||||||
|
|
||||||
|
**Resultado:** Presentación utilizada por el equipo comercial para prospección.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultados e Impacto
|
||||||
|
|
||||||
|
### Ventas Realizadas
|
||||||
|
|
||||||
|
**Cliente 1: Aseguradora**
|
||||||
|
- Stack: ASP 3.0, VB6 components, .NET, microservicios
|
||||||
|
- Alcance: Migración completa de aplicaciones legacy
|
||||||
|
- Estado: **Proyecto vendido** (ejecución por otro equipo)
|
||||||
|
- Valor: [Confidencial]
|
||||||
|
|
||||||
|
**Cliente 2: Empresa de Cobranza**
|
||||||
|
- Alcance: Migración de base de datos (~100M registros)
|
||||||
|
- Estado: **Proyecto vendido y en ejecución** (por mí)
|
||||||
|
- Particularidad: Proceso fue **reestructurado** para atender necesidades específicas
|
||||||
|
- Ver caso completo: [Migración CNPJ - 100M Registros](/cases/cnpj-migration-database)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto en el Negocio
|
||||||
|
|
||||||
|
💰 **2 proyectos vendidos** antes incluso de la primera ejecución
|
||||||
|
📈 **Proceso replicable** para nuevos clientes
|
||||||
|
🎯 **Posicionamiento** como especialista en migraciones regulatorias
|
||||||
|
📚 **Base de conocimiento** para futuros proyectos similares
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto Técnico
|
||||||
|
|
||||||
|
🔧 **Metodología probada** en escenarios reales
|
||||||
|
📖 **Documentación reutilizable** (scripts, checklists, templates)
|
||||||
|
🚀 **Aceleración** de proyectos similares (de semanas a días)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`SQL Server` `Migration Strategy` `Process Design` `Regulatory Compliance` `ASP 3.0` `VB6` `.NET` `Microservices` `Batch Processing` `Database Optimization`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisiones Clave & Trade-offs
|
||||||
|
|
||||||
|
### ¿Por qué proceso estructurado?
|
||||||
|
|
||||||
|
**Alternativas:**
|
||||||
|
1. ❌ Enfoque ad-hoc por proyecto
|
||||||
|
2. ❌ Consultoría manual sin metodología
|
||||||
|
3. ✅ **Proceso replicable y escalable**
|
||||||
|
|
||||||
|
**Justificación:**
|
||||||
|
- Reduce tiempo de Discovery
|
||||||
|
- Estandariza entregas
|
||||||
|
- Facilita ventas (presentación lista)
|
||||||
|
- Permite ejecución por diferentes equipos
|
||||||
|
|
||||||
|
### ¿Por qué separar en 5 fases?
|
||||||
|
|
||||||
|
**Beneficios:**
|
||||||
|
- Cliente puede aprobar fase a fase
|
||||||
|
- Permite ajustes durante el proceso
|
||||||
|
- Facilita gestión de riesgos
|
||||||
|
- Entregas incrementales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecciones Aprendidas
|
||||||
|
|
||||||
|
### 1. UX/Presentación Importa para Ventas
|
||||||
|
|
||||||
|
La presentación visual hecha por el gestor de UX fue **crucial** para cerrar los 2 contratos. Proceso técnico bueno + presentación mala = sin ventas.
|
||||||
|
|
||||||
|
### 2. Proceso Vende, No Solo Ejecución
|
||||||
|
|
||||||
|
Crear una **metodología documentada** tiene más valor comercial que solo ofrecer "horas de consultoría".
|
||||||
|
|
||||||
|
### 3. Cada Cliente es Único
|
||||||
|
|
||||||
|
El cliente solicitó **reestructuración del proceso**. Un buen proceso debe ser:
|
||||||
|
- Estructurado lo suficiente para ser replicable
|
||||||
|
- Flexible lo suficiente para personalizar
|
||||||
|
|
||||||
|
### 4. Colaboración Multidisciplinaria
|
||||||
|
|
||||||
|
Trabajar con gestor de UX (presentaciones) + equipo comercial (ventas) + técnico (ejecución) = éxito.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Próximos Pasos
|
||||||
|
|
||||||
|
**Oportunidades futuras:**
|
||||||
|
|
||||||
|
1. 🌎 **Expansión:** Ofrecer CNPJ Fast para más sectores (bancos, fintechs, retail)
|
||||||
|
2. 📦 **Producto:** Transformar en herramienta automatizada (SaaS)
|
||||||
|
3. 📚 **Capacitación:** Capacitar equipos internos de clientes
|
||||||
|
4. 🔄 **Evolución:** Adaptar proceso para otras migraciones regulatorias (PIX, Open Banking)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** Metodología estructurada que se convirtió en producto vendible, generando ingresos antes incluso de la primera ejecución técnica.
|
||||||
|
|
||||||
|
[¿Quiere implementar CNPJ Fast en su empresa? Póngase en contacto](#contact)
|
||||||
469
Content/Cases/es/cnpj-migration-database.md
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
---
|
||||||
|
title: "Migración CNPJ Alfanumérico - 100 Millones de Registros"
|
||||||
|
slug: "cnpj-migration-database"
|
||||||
|
summary: "Ejecución de migración masiva de CNPJ numérico a alfanumérico en base de datos con ~100M registros, usando estrategia de commits faseados para evitar bloqueo."
|
||||||
|
client: "Empresa de Cobranza"
|
||||||
|
industry: "Cobranza & Servicios Financieros"
|
||||||
|
timeline: "En ejecución"
|
||||||
|
role: "Database Architect & Tech Lead"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- SQL Server
|
||||||
|
- Database Migration
|
||||||
|
- CNPJ
|
||||||
|
- Performance Optimization
|
||||||
|
- Batch Processing
|
||||||
|
- Big Data
|
||||||
|
featured: true
|
||||||
|
order: 4
|
||||||
|
date: 2024-11-01
|
||||||
|
seo_title: "Migración CNPJ Alfanumérico - 100M Registros | Carneiro Tech"
|
||||||
|
seo_description: "Caso de migración masiva de CNPJ en base de datos con 100 millones de registros usando commits faseados y optimizaciones de performance."
|
||||||
|
seo_keywords: "database migration, SQL Server, CNPJ, batch processing, performance optimization, phased commits"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
Una empresa de cobranza que trabaja con bases de datos de información transitoria (sin software propietario) necesita adaptar sus sistemas al nuevo formato de **CNPJ alfanumérico** brasileño.
|
||||||
|
|
||||||
|
**Desafío principal:** Migrar ~**100 millones de registros** en tablas con columnas `BIGINT` y `NUMERIC` a `VARCHAR`, sin bloquear la base de datos en producción.
|
||||||
|
|
||||||
|
**Estado:** Proyecto en ejecución (preparación de scripts de migración).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafío
|
||||||
|
|
||||||
|
### Volumen Masivo de Datos
|
||||||
|
|
||||||
|
**Contexto de la empresa:**
|
||||||
|
- Empresa de cobranza (no desarrolla software propio)
|
||||||
|
- Trabaja con **datos transitorios** (alta rotación)
|
||||||
|
- Base de datos SQL Server con volumen crítico
|
||||||
|
|
||||||
|
**Análisis inicial reveló:**
|
||||||
|
|
||||||
|
| Tabla | Columna | Tipo Actual | Registros | Tamaño |
|
||||||
|
|--------|--------|------------|-----------|---------|
|
||||||
|
| Deudores | CNPJ_Deudor | BIGINT | 8.000.000 | 60 GB |
|
||||||
|
| Transacciones | CNPJ_Pagador | NUMERIC(14) | 90.000.000 | 1.2 TB |
|
||||||
|
| Empresas | CNPJ_Empresa | BIGINT | 2.500.000 | 18 GB |
|
||||||
|
| **TOTAL** | - | - | **~100.000.000** | **~1.3 TB** |
|
||||||
|
|
||||||
|
**Problemas identificados:**
|
||||||
|
|
||||||
|
1. 🔴 **Tablas con 8M+ líneas** usando `BIGINT` para CNPJ
|
||||||
|
2. 🔴 **90 millones de registros** en tabla de transacciones
|
||||||
|
3. 🔑 **CNPJ como clave primaria** en algunas tablas
|
||||||
|
4. 🔗 **Foreign keys** relacionando múltiples tablas
|
||||||
|
5. ⚠️ **Imposibilidad de downtime prolongado** (operación 24/7)
|
||||||
|
6. 💾 **Restricciones de espacio** en disco (necesita estrategia eficiente)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisión Estratégica: Phased Commits
|
||||||
|
|
||||||
|
### ¿Por qué NO hacer ALTER COLUMN directo?
|
||||||
|
|
||||||
|
**Enfoque ingenuo (NO funciona):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ❌ NUNCA HAGA ESTO EN TABLAS GRANDES
|
||||||
|
ALTER TABLE Transacciones
|
||||||
|
ALTER COLUMN CNPJ_Pagador VARCHAR(18);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problemas:**
|
||||||
|
- 🔒 Bloquea la tabla entera durante la conversión
|
||||||
|
- ⏱️ Puede tomar horas/días en tablas grandes
|
||||||
|
- 💥 Bloquea todas las operaciones (INSERT, UPDATE, SELECT)
|
||||||
|
- 🚨 Riesgo de timeout o falla en medio de la operación
|
||||||
|
- 🔙 Rollback complejo si algo sale mal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Estrategia Elegida: Column Swap con Commits Faseados
|
||||||
|
|
||||||
|
**Basado en experiencia anterior**, decidí usar enfoque gradual:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 1. Crear nueva columna VARCHAR al FINAL │
|
||||||
|
│ (operación rápida, no bloquea tabla) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 2. UPDATE en lotes (commits faseados) │
|
||||||
|
│ - 100k registros a la vez │
|
||||||
|
│ - Pausa entre lotes (evita contención) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 3. Remover PKs y FKs │
|
||||||
|
│ (tras 100% migrado) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 4. Renombrar columnas (swap) │
|
||||||
|
│ - CNPJ → CNPJ_Old │
|
||||||
|
│ - CNPJ_New → CNPJ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 5. Recrear PKs/FKs con nueva columna │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 6. Validación y eliminación columna vieja │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**¿Por qué este enfoque?**
|
||||||
|
|
||||||
|
✅ **Sin lock de tabla completa** (operación incremental)
|
||||||
|
✅ **Puede pausar/reanudar** en cualquier momento
|
||||||
|
✅ **Monitoreo de progreso** en tiempo real
|
||||||
|
✅ **Rollback simple** (basta eliminar nueva columna)
|
||||||
|
✅ **Minimiza impacto en producción** (commits pequeños)
|
||||||
|
|
||||||
|
**Decisión tomada basada en:**
|
||||||
|
- 📚 Experiencia anterior con migraciones de gran volumen
|
||||||
|
- 🔍 Conocimiento de locks de SQL Server
|
||||||
|
- 🎯 Necesidad de zero downtime
|
||||||
|
|
||||||
|
**Nota:** Esta decisión fue tomada **sin consultar IA** - basada puramente en experiencia práctica de proyectos anteriores.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detalles de Implementación
|
||||||
|
|
||||||
|
### Fase 1: Crear Nueva Columna
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Operación rápida (metadata change solamente)
|
||||||
|
ALTER TABLE Transacciones
|
||||||
|
ADD CNPJ_Pagador_New VARCHAR(18) NULL;
|
||||||
|
|
||||||
|
-- Agrega índice temporal para acelerar lookups
|
||||||
|
CREATE NONCLUSTERED INDEX IX_Temp_CNPJ_New
|
||||||
|
ON Transacciones(CNPJ_Pagador_New)
|
||||||
|
WHERE CNPJ_Pagador_New IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tiempo estimado:** ~1 segundo (independiente del tamaño de la tabla)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Migración en Lotes (Core Strategy)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Script de migración con commits faseados
|
||||||
|
DECLARE @BatchSize INT = 100000; -- 100k registros por lote
|
||||||
|
DECLARE @RowsAffected INT = 1;
|
||||||
|
DECLARE @TotalProcessed INT = 0;
|
||||||
|
DECLARE @StartTime DATETIME = GETDATE();
|
||||||
|
|
||||||
|
WHILE @RowsAffected > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Actualiza lote de 100k registros aún no migrados
|
||||||
|
UPDATE TOP (@BatchSize) Transacciones
|
||||||
|
SET CNPJ_Pagador_New = RIGHT('00000000000000' + CAST(CNPJ_Pagador AS VARCHAR), 14)
|
||||||
|
WHERE CNPJ_Pagador_New IS NULL;
|
||||||
|
|
||||||
|
SET @RowsAffected = @@ROWCOUNT;
|
||||||
|
SET @TotalProcessed = @TotalProcessed + @RowsAffected;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
|
||||||
|
-- Log de progreso
|
||||||
|
PRINT 'Processed: ' + CAST(@TotalProcessed AS VARCHAR) + ' rows. Batch: ' + CAST(@RowsAffected AS VARCHAR);
|
||||||
|
PRINT 'Elapsed time: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR) + ' seconds';
|
||||||
|
|
||||||
|
-- Pausa entre lotes (reduce contención)
|
||||||
|
WAITFOR DELAY '00:00:01'; -- 1 segundo entre lotes
|
||||||
|
END;
|
||||||
|
|
||||||
|
PRINT 'Migration completed! Total rows: ' + CAST(@TotalProcessed AS VARCHAR);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parámetros configurables:**
|
||||||
|
|
||||||
|
- `@BatchSize`: 100k (balanceado entre performance y lock time)
|
||||||
|
- Muy pequeño = muchas transacciones, overhead
|
||||||
|
- Muy grande = lock prolongado, impacto en prod
|
||||||
|
- `WAITFOR DELAY`: 1 segundo (da tiempo a otras queries para ejecutar)
|
||||||
|
|
||||||
|
**Estimaciones de tiempo:**
|
||||||
|
|
||||||
|
| Registros | Batch Size | Tiempo Estimado |
|
||||||
|
|-----------|------------|----------------|
|
||||||
|
| 8.000.000 | 100.000 | ~2-3 horas |
|
||||||
|
| 90.000.000 | 100.000 | ~20-24 horas |
|
||||||
|
|
||||||
|
**Ventajas:**
|
||||||
|
- ✅ No bloquea aplicación
|
||||||
|
- ✅ Otras queries pueden ejecutar entre los lotes
|
||||||
|
- ✅ Puede pausar (Ctrl+C) y reanudar después (WHERE NULL toma desde donde paró)
|
||||||
|
- ✅ Log de progreso en tiempo real
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Remoción de Constraints
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Identifica todas las PKs y FKs que involucran la columna
|
||||||
|
SELECT name
|
||||||
|
FROM sys.key_constraints
|
||||||
|
WHERE type = 'PK'
|
||||||
|
AND parent_object_id = OBJECT_ID('Transacciones')
|
||||||
|
AND COL_NAME(parent_object_id, parent_column_id) = 'CNPJ_Pagador';
|
||||||
|
|
||||||
|
-- Remueve PKs
|
||||||
|
ALTER TABLE Transacciones
|
||||||
|
DROP CONSTRAINT PK_Transacciones_CNPJ;
|
||||||
|
|
||||||
|
-- Remueve FKs (tablas que referencian)
|
||||||
|
ALTER TABLE Pagos
|
||||||
|
DROP CONSTRAINT FK_Pagos_Transacciones;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tiempo estimado:** ~10 minutos (depende de cuántas constraints existen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4: Column Swap (Renombramiento)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Renombra columna antigua a _Old
|
||||||
|
EXEC sp_rename 'Transacciones.CNPJ_Pagador', 'CNPJ_Pagador_Old', 'COLUMN';
|
||||||
|
|
||||||
|
-- Renombra nueva columna al nombre original
|
||||||
|
EXEC sp_rename 'Transacciones.CNPJ_Pagador_New', 'CNPJ_Pagador', 'COLUMN';
|
||||||
|
|
||||||
|
-- Altera a NOT NULL (tras validación de 100% completado)
|
||||||
|
ALTER TABLE Transacciones
|
||||||
|
ALTER COLUMN CNPJ_Pagador VARCHAR(18) NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tiempo estimado:** ~1 segundo (metadata change)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 5: Recreación de Constraints
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recrea PK con nueva columna VARCHAR
|
||||||
|
ALTER TABLE Transacciones
|
||||||
|
ADD CONSTRAINT PK_Transacciones_CNPJ
|
||||||
|
PRIMARY KEY CLUSTERED (CNPJ_Pagador);
|
||||||
|
|
||||||
|
-- Recrea FKs
|
||||||
|
ALTER TABLE Pagos
|
||||||
|
ADD CONSTRAINT FK_Pagos_Transacciones
|
||||||
|
FOREIGN KEY (CNPJ_Pagador) REFERENCES Transacciones(CNPJ_Pagador);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tiempo estimado:** ~30-60 minutos (depende del volumen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 6: Validación y Limpieza
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Valida que 100% fue migrado
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM Transacciones
|
||||||
|
WHERE CNPJ_Pagador IS NULL OR CNPJ_Pagador = '';
|
||||||
|
|
||||||
|
-- Valida integridad referencial
|
||||||
|
DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS;
|
||||||
|
|
||||||
|
-- Si todo OK, remueve columna antigua
|
||||||
|
ALTER TABLE Transacciones
|
||||||
|
DROP COLUMN CNPJ_Pagador_Old;
|
||||||
|
|
||||||
|
-- Remueve índice temporal
|
||||||
|
DROP INDEX IX_Temp_CNPJ_New ON Transacciones;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Personalización del Proceso CNPJ Fast
|
||||||
|
|
||||||
|
### Diferencias vs. Proceso Original
|
||||||
|
|
||||||
|
El proceso **CNPJ Fast** original fue **reestructurado** para este cliente:
|
||||||
|
|
||||||
|
**Cambios principales:**
|
||||||
|
|
||||||
|
| Aspecto | CNPJ Fast Original | Cliente (Personalizado) |
|
||||||
|
|---------|-------------------|---------------------|
|
||||||
|
| **Foco** | Aplicaciones + DB | Solo DB (sin software propio) |
|
||||||
|
| **Discovery** | Inventario de apps | Solo análisis de schema |
|
||||||
|
| **Ejecución** | Múltiples aplicaciones | Scripts SQL masivos |
|
||||||
|
| **Batch Size** | 50k-100k | 100k (optimizado para volumen) |
|
||||||
|
| **Monitoreo** | Manual + herramientas | Logs SQL en tiempo real |
|
||||||
|
| **Rollback** | Proceso complejo | Simple (DROP COLUMN) |
|
||||||
|
|
||||||
|
**Motivo de la reestructuración:**
|
||||||
|
- Cliente no tiene aplicaciones propias (solo consume datos)
|
||||||
|
- Foco 100% en optimización de base de datos
|
||||||
|
- Volumen mucho mayor que casos típicos (100M vs ~10M)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`SQL Server` `T-SQL` `Batch Processing` `Performance Tuning` `Database Optimization` `Migration Scripts` `Phased Commits` `Index Optimization` `Constraint Management`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisiones Clave & Trade-offs
|
||||||
|
|
||||||
|
### ¿Por qué 100k por batch?
|
||||||
|
|
||||||
|
**Pruebas de performance:**
|
||||||
|
|
||||||
|
| Batch Size | Tiempo/Batch | Lock Duration | Contención |
|
||||||
|
|------------|-------------|---------------|-----------|
|
||||||
|
| 10.000 | 2s | Bajo | ✅ Mínimo |
|
||||||
|
| 50.000 | 8s | Medio | ✅ Aceptable |
|
||||||
|
| **100.000** | 15s | **Medio** | **✅ Balanceado** |
|
||||||
|
| 500.000 | 90s | Alto | ❌ Impacto en prod |
|
||||||
|
| 1.000.000 | 180s | Muy alto | ❌ Inaceptable |
|
||||||
|
|
||||||
|
**Elección:** 100k ofrece mejor balance entre performance e impacto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ¿Por qué crear columna al FINAL?
|
||||||
|
|
||||||
|
**Internals de SQL Server:**
|
||||||
|
- Agregar columna al final = metadata change (rápido)
|
||||||
|
- Agregar en medio = reescritura de páginas (lento)
|
||||||
|
- Para tablas grandes, posición importa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ¿Por qué WAITFOR DELAY de 1 segundo?
|
||||||
|
|
||||||
|
**Sin delay:**
|
||||||
|
- ❌ Batch processing consume 100% del I/O
|
||||||
|
- ❌ Queries de aplicación se vuelven lentas
|
||||||
|
- ❌ Lock escalation puede ocurrir
|
||||||
|
|
||||||
|
**Con delay de 1s:**
|
||||||
|
- ✅ Otras queries tienen ventana para ejecutar
|
||||||
|
- ✅ I/O distribuido
|
||||||
|
- ✅ Experiencia del usuario preservada
|
||||||
|
|
||||||
|
**Trade-off:** Migración toma +1s por batch (~25% más lenta), pero sistema permanece responsivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estado Actual & Próximos Pasos
|
||||||
|
|
||||||
|
### Estado Actual (Diciembre 2024)
|
||||||
|
|
||||||
|
📝 **Fase de Preparación:**
|
||||||
|
- ✅ Discovery completo (100M registros identificados)
|
||||||
|
- ✅ Scripts de migración desarrollados
|
||||||
|
- ✅ Pruebas en ambiente de homologación
|
||||||
|
- 🔄 Validación de performance
|
||||||
|
- ⏳ Esperando ventana de mantenimiento para producción
|
||||||
|
|
||||||
|
### Próximos Pasos
|
||||||
|
|
||||||
|
1. **Backup completo** de producción
|
||||||
|
2. **Ejecución en producción** (ambiente 24/7)
|
||||||
|
3. **Monitoreo en tiempo real** durante migración
|
||||||
|
4. **Validación post-migración** (integridad, performance)
|
||||||
|
5. **Documentación de lessons learned**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecciones Aprendidas (Hasta Ahora)
|
||||||
|
|
||||||
|
### 1. Experiencia Anterior Vale Oro
|
||||||
|
|
||||||
|
Decisión de usar phased commits vino de **experiencia práctica** en proyectos anteriores, no de documentación o IA.
|
||||||
|
|
||||||
|
**Situaciones similares anteriores:**
|
||||||
|
- Migración de datos en e-commerce (50M registros)
|
||||||
|
- Conversión de encoding (UTF-8 en 100M+ rows)
|
||||||
|
- Particionamiento de tablas históricas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. "Measure Twice, Cut Once"
|
||||||
|
|
||||||
|
Antes de ejecutar en producción:
|
||||||
|
- ✅ Pruebas exhaustivas en homologación
|
||||||
|
- ✅ Scripts validados y revisados
|
||||||
|
- ✅ Rollback probado
|
||||||
|
- ✅ Estimaciones de tiempo confirmadas
|
||||||
|
|
||||||
|
**Tiempo de preparación:** 3 semanas
|
||||||
|
**Tiempo de ejecución:** Estimado en 48 horas
|
||||||
|
|
||||||
|
**Ratio:** 10:1 (preparación vs ejecución)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Personalización > One-Size-Fits-All
|
||||||
|
|
||||||
|
El proceso CNPJ Fast original necesitó ser **reestructurado** para este cliente.
|
||||||
|
|
||||||
|
**Lección:** Los procesos deben ser:
|
||||||
|
- Estructurados lo suficiente para repetir
|
||||||
|
- Flexibles lo suficiente para adaptar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Monitoreo es Crucial
|
||||||
|
|
||||||
|
Scripts con **log detallado** de progreso permiten:
|
||||||
|
- Estimar tiempo restante
|
||||||
|
- Identificar cuellos de botella
|
||||||
|
- Pausar/reanudar con confianza
|
||||||
|
- Reportar estado a stakeholders
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Log example
|
||||||
|
Processed: 10.000.000 rows. Batch: 100.000
|
||||||
|
Elapsed time: 3600 seconds (10% complete, ~9h remaining)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Optimizaciones de Performance
|
||||||
|
|
||||||
|
### Optimizaciones Implementadas
|
||||||
|
|
||||||
|
1. **Índice temporal WHERE NULL**
|
||||||
|
- Acelera lookup de registros no migrados
|
||||||
|
- Removido tras conclusión
|
||||||
|
|
||||||
|
2. **Batch size optimizado**
|
||||||
|
- Balanceado entre performance y lock time
|
||||||
|
|
||||||
|
3. **Transaction log management**
|
||||||
|
```sql
|
||||||
|
-- Verificar crecimiento del log
|
||||||
|
DBCC SQLPERF(LOGSPACE);
|
||||||
|
|
||||||
|
-- Ajustar recovery model (si permitido)
|
||||||
|
ALTER DATABASE MyDatabase SET RECOVERY SIMPLE;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Ejecución en horario de menor carga**
|
||||||
|
- Ventana de mantenimiento nocturna
|
||||||
|
- Fin de semana (si es posible)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado esperado:** Migración de 100 millones de registros en ~48 horas, sin downtime significativo y con posibilidad de rollback rápido.
|
||||||
|
|
||||||
|
[¿Necesita migrar volúmenes masivos de datos? Póngase en contacto](#contact)
|
||||||
588
Content/Cases/es/industrial-learning-platform.md
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
---
|
||||||
|
title: "Plataforma de Capacitación Industrial - De Wireframes a Sistema Completo"
|
||||||
|
slug: "industrial-learning-platform"
|
||||||
|
summary: "Solution Design para plataforma de microlearning en empresa de gases industriales. Identificación de requisitos críticos no mapeados (admin, registros, exportación) antes de la presentación al cliente, evitando retrabajo y garantizando usabilidad real."
|
||||||
|
client: "Empresa de Gases Industriales"
|
||||||
|
industry: "Industrial & Manufactura"
|
||||||
|
timeline: "4 meses"
|
||||||
|
role: "Solution Architect & Tech Lead"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- Solution Design
|
||||||
|
- EdTech
|
||||||
|
- Learning Platform
|
||||||
|
- Requirements Analysis
|
||||||
|
- Tech Lead
|
||||||
|
- User Stories
|
||||||
|
- .NET
|
||||||
|
- Product Design
|
||||||
|
featured: true
|
||||||
|
order: 5
|
||||||
|
date: 2024-06-01
|
||||||
|
seo_title: "Plataforma de Capacitación Industrial - Solution Design"
|
||||||
|
seo_description: "Caso de Solution Design para plataforma de microlearning, identificando requisitos críticos antes de la presentación al cliente y liderando desarrollo hasta producción."
|
||||||
|
seo_keywords: "solution design, learning platform, microlearning, requirements analysis, tech lead, industrial training"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
Empresa de gases industriales solicita plataforma para capacitar empleados usando metodología de **microlearning** (contenidos cortos y objetivos).
|
||||||
|
|
||||||
|
**Requisito inicial:** "Queremos solo la estructura - ruta de aprendizaje, microlearning, pregunta de test y puntuación."
|
||||||
|
|
||||||
|
**Problema:** Especificación incompleta que resultaría en sistema **imposible de usar** (sin forma de registrar contenido, sin administradores, sin exportar resultados).
|
||||||
|
|
||||||
|
**Solución:** Análisis crítico de requisitos **antes de la presentación al cliente**, identificando gaps funcionales y proponiendo solución completa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafío
|
||||||
|
|
||||||
|
### Wireframes Bonitos, Funcionalidad Incompleta
|
||||||
|
|
||||||
|
**Situación inicial:**
|
||||||
|
|
||||||
|
UX creó wireframes hermosos mostrando:
|
||||||
|
- ✅ Rutas de aprendizaje
|
||||||
|
- ✅ Microlearnings (video/texto + imagen)
|
||||||
|
- ✅ Preguntas de test (opción múltiple)
|
||||||
|
- ✅ Puntuación por empleado
|
||||||
|
|
||||||
|
**Problema identificado:**
|
||||||
|
|
||||||
|
Nadie (cliente, UX, comercial) pensó en:
|
||||||
|
|
||||||
|
❌ **¿Cómo entra contenido en el sistema?**
|
||||||
|
- ¿Quién registra rutas?
|
||||||
|
- ¿Quién crea microlearnings?
|
||||||
|
- ¿Quién escribe preguntas?
|
||||||
|
- ¿Interfaz manual o import?
|
||||||
|
|
||||||
|
❌ **¿Quién gestiona el sistema?**
|
||||||
|
- ¿Existe concepto de admin?
|
||||||
|
- ¿RRHH puede crear admins?
|
||||||
|
- ¿Gestor de área puede ver solo su equipo?
|
||||||
|
|
||||||
|
❌ **¿Cómo salen datos del sistema?**
|
||||||
|
- RRHH necesita reportes
|
||||||
|
- Compliance necesita evidencias
|
||||||
|
- ¿Cómo exportar datos?
|
||||||
|
- ¿Formato: Excel? PDF? API?
|
||||||
|
|
||||||
|
**Riesgo real:**
|
||||||
|
|
||||||
|
Si desarrolláramos exactamente lo que fue pedido:
|
||||||
|
- Sistema funcionaría técnicamente ✅
|
||||||
|
- **Pero sería completamente inutilizable** ❌
|
||||||
|
- Cliente tendría que pagar refacción para incluir CRUD básico
|
||||||
|
- Retrabajo + costo adicional + frustración
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proceso de Solution Design
|
||||||
|
|
||||||
|
### Etapa 1: Análisis Crítico (Antes de la Presentación)
|
||||||
|
|
||||||
|
**Acción tomada:** Convoqué reunión con UX **antes** de presentar al cliente.
|
||||||
|
|
||||||
|
**Puntos levantados:**
|
||||||
|
|
||||||
|
**"¿Cómo entra el primer contenido al sistema?"**
|
||||||
|
- UX: "Ah... no pensamos en eso. ¿Ustedes van a poblar la base de datos?"
|
||||||
|
- Yo: "¿Y cuando cliente quiera agregar nueva ruta? ¿Vamos a alterar BD en producción?"
|
||||||
|
|
||||||
|
**"¿Quién es el dueño del sistema?"**
|
||||||
|
- UX: "RRHH, imagino."
|
||||||
|
- Yo: "¿Solo una persona? ¿Y si sale de la empresa? ¿Cómo delega?"
|
||||||
|
|
||||||
|
**"¿RRHH pidió reportes?"**
|
||||||
|
- UX: "No fue mencionado en el briefing."
|
||||||
|
- Yo: "RRHH siempre necesita reportes. Es para compliance (NR, ISO)."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Etapa 2: Requisitos Funcionales Identificados
|
||||||
|
|
||||||
|
Propuse 4 módulos adicionales **esenciales**:
|
||||||
|
|
||||||
|
#### 1. Sistema de Administración
|
||||||
|
|
||||||
|
**Funcionalidades:**
|
||||||
|
- Usuario estándar: Solo realiza capacitaciones
|
||||||
|
- Usuario admin: Gestiona contenido + ve reportes
|
||||||
|
- Admin puede promover otros usuarios a admin
|
||||||
|
- Control de acceso (admin general vs admin de área)
|
||||||
|
|
||||||
|
**Por qué es crítico:**
|
||||||
|
Sin esto, sistema es estático (contenido nunca se actualiza).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. CRUD de Contenido
|
||||||
|
|
||||||
|
**a) Registro de Rutas:**
|
||||||
|
- Nombre de la ruta
|
||||||
|
- Descripción
|
||||||
|
- Orden de los microlearnings
|
||||||
|
- Ruta activa/inactiva (permite despublicar)
|
||||||
|
|
||||||
|
**b) Registro de Microlearnings:**
|
||||||
|
- Título
|
||||||
|
- Tipo: Texto simple (2 párrafos) O Video
|
||||||
|
- Upload de imagen (si texto)
|
||||||
|
- URL de video (si video)
|
||||||
|
- Orden dentro de la ruta
|
||||||
|
|
||||||
|
**c) Registro de Preguntas:**
|
||||||
|
- Pregunta (texto)
|
||||||
|
- 3 opciones de respuesta:
|
||||||
|
- "Excelente" (verde)
|
||||||
|
- "Regular" (amarillo)
|
||||||
|
- "Malo" (rojo)
|
||||||
|
- Puntuación por respuesta (ej: 10, 5, 0 puntos)
|
||||||
|
- Feedback personalizado por respuesta
|
||||||
|
|
||||||
|
**Por qué es crítico:**
|
||||||
|
Cliente necesita actualizar contenido sin llamar a dev/DBA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Exportación de Datos
|
||||||
|
|
||||||
|
**Funcionalidades:**
|
||||||
|
- Exportar a Excel (.xlsx)
|
||||||
|
- Filtros:
|
||||||
|
- Por período (fecha inicio/fin)
|
||||||
|
- Por ruta
|
||||||
|
- Por empleado
|
||||||
|
- Por área/departamento
|
||||||
|
- Columnas exportadas:
|
||||||
|
- Nombre del empleado
|
||||||
|
- Matrícula
|
||||||
|
- Ruta completada
|
||||||
|
- Puntuación total
|
||||||
|
- Fecha de conclusión
|
||||||
|
- Respuestas individuales (para auditoría)
|
||||||
|
|
||||||
|
**Por qué es crítico:**
|
||||||
|
RRHH necesita evidenciar capacitación para:
|
||||||
|
- Normas Reglamentarias (NR-13, NR-20 - gases inflamables)
|
||||||
|
- Auditorías ISO
|
||||||
|
- Procesos laborales
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. Gestión de Usuarios
|
||||||
|
|
||||||
|
**Funcionalidades:**
|
||||||
|
- Importar empleados (upload CSV/Excel)
|
||||||
|
- Registro manual
|
||||||
|
- Activar/desactivar usuarios
|
||||||
|
- Asignar rutas obligatorias por área
|
||||||
|
- Notificaciones de pendientes
|
||||||
|
|
||||||
|
**Por qué es crítico:**
|
||||||
|
Empresa tiene 500+ empleados, registro manual es inviable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Etapa 3: Presentación al Cliente
|
||||||
|
|
||||||
|
**Abordaje:**
|
||||||
|
|
||||||
|
1. Mostré wireframes del UX (interfaz bonita)
|
||||||
|
2. Pregunté: "¿Cómo van a registrar la primera ruta?"
|
||||||
|
3. Cliente: "Ah... buena pregunta. No habíamos pensado en eso."
|
||||||
|
4. Presenté los 4 módulos adicionales
|
||||||
|
5. Cliente: "Tiene total sentido! Sin esto no podemos usar."
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
- Propuesta aprobada **con módulos adicionales**
|
||||||
|
- Alcance ajustado (timeline + presupuesto)
|
||||||
|
- Zero retrabajo futuro
|
||||||
|
- Cliente reconoció valor agregado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementación
|
||||||
|
|
||||||
|
### Mi Rol en el Proyecto
|
||||||
|
|
||||||
|
**1. Solution Architect**
|
||||||
|
- Identificación de requisitos no funcionales
|
||||||
|
- Diseño de arquitectura (módulos, integraciones)
|
||||||
|
- Definición de tecnologías
|
||||||
|
|
||||||
|
**2. Tech Lead**
|
||||||
|
- Liderazgo técnico del equipo (3 devs)
|
||||||
|
- Code review
|
||||||
|
- Definición de estándares de código
|
||||||
|
- Gestión de riesgos técnicos
|
||||||
|
|
||||||
|
**3. Product Owner Técnico**
|
||||||
|
- Creación de **user stories** completas
|
||||||
|
- Priorización de backlog
|
||||||
|
- Refinamiento continuo con cliente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stack Técnico Elegido
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `.NET 7` - APIs REST
|
||||||
|
- `Entity Framework Core` - ORM
|
||||||
|
- `SQL Server` - Base de datos
|
||||||
|
- `ClosedXML` - Generación de Excel
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `React` - Interfaz web
|
||||||
|
- `Material-UI` - Componentes
|
||||||
|
- `React Player` - Player de video
|
||||||
|
- `Chart.js` - Gráficos de progreso
|
||||||
|
|
||||||
|
**Infraestructura:**
|
||||||
|
- `Azure App Service` - Hospedaje
|
||||||
|
- `Azure Blob Storage` - Almacenamiento de videos/imágenes
|
||||||
|
- `Azure SQL Database` - Base de datos gestionada
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Stories Creadas
|
||||||
|
|
||||||
|
Escribí **32 user stories** cubriendo todos los flujos. Ejemplos:
|
||||||
|
|
||||||
|
**US-01: Registrar Ruta (Admin)**
|
||||||
|
```
|
||||||
|
Como administrador del sistema
|
||||||
|
Quiero registrar una nueva ruta de capacitación
|
||||||
|
Para que empleados puedan realizar los cursos
|
||||||
|
|
||||||
|
Criterios de aceptación:
|
||||||
|
- Admin accede menú "Rutas" → "Nueva Ruta"
|
||||||
|
- Completa: Nombre, Descripción, Estado (Activa/Inactiva)
|
||||||
|
- Puede agregar microlearnings existentes a la ruta
|
||||||
|
- Define orden de los microlearnings (drag & drop)
|
||||||
|
- Sistema valida campos obligatorios
|
||||||
|
- Guarda y muestra mensaje de éxito
|
||||||
|
```
|
||||||
|
|
||||||
|
**US-15: Realizar Microlearning (Empleado)**
|
||||||
|
```
|
||||||
|
Como empleado
|
||||||
|
Quiero realizar un microlearning de mi ruta
|
||||||
|
Para aprender sobre el tema y ganar puntos
|
||||||
|
|
||||||
|
Criterios de aceptación:
|
||||||
|
- Empleado accede ruta asignada
|
||||||
|
- Ve lista de microlearnings (no completados primero)
|
||||||
|
- Hace clic en microlearning → abre pantalla con:
|
||||||
|
- Texto (2 párrafos) + Imagen O
|
||||||
|
- Player de video embebido
|
||||||
|
- Botón "Continuar" aparece después de:
|
||||||
|
- 30s (si texto)
|
||||||
|
- Final del video (si video)
|
||||||
|
- Marca microlearning como visto
|
||||||
|
- Pregunta de test aparece automáticamente
|
||||||
|
```
|
||||||
|
|
||||||
|
**US-22: Exportar Resultados (Admin)**
|
||||||
|
```
|
||||||
|
Como administrador
|
||||||
|
Quiero exportar resultados de capacitación a Excel
|
||||||
|
Para generar reportes de compliance y auditorías
|
||||||
|
|
||||||
|
Criterios de aceptación:
|
||||||
|
- Admin accede "Reportes" → "Exportar"
|
||||||
|
- Selecciona filtros (período, ruta, área)
|
||||||
|
- Hace clic "Generar Excel"
|
||||||
|
- Sistema procesa y descarga archivo .xlsx
|
||||||
|
- Excel contiene columnas: Nombre, Matrícula, Ruta, Puntos, Fecha, Respuestas
|
||||||
|
- Formato legible (headers en negrita, columnas autoajustadas)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Características Clave Implementadas
|
||||||
|
|
||||||
|
### 1. Sistema de Puntuación Gamificado
|
||||||
|
|
||||||
|
**Mecánica:**
|
||||||
|
- Cada pregunta vale puntos (configurable)
|
||||||
|
- Respuesta "Excelente": 10 puntos
|
||||||
|
- Respuesta "Regular": 5 puntos
|
||||||
|
- Respuesta "Malo": 0 puntos
|
||||||
|
|
||||||
|
**Dashboard del empleado:**
|
||||||
|
- Puntuación total
|
||||||
|
- Ranking (opcional, configurable)
|
||||||
|
- Badges por rutas completadas
|
||||||
|
- Progreso visual (barra de %)
|
||||||
|
|
||||||
|
**Por qué funciona:**
|
||||||
|
Empleados de planta se enganchan más con elementos de gamificación.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Microlearning Adaptativo
|
||||||
|
|
||||||
|
**Tipos de contenido:**
|
||||||
|
|
||||||
|
**Texto + Imagen:**
|
||||||
|
- 2 párrafos (máx 300 palabras)
|
||||||
|
- 1 imagen ilustrativa
|
||||||
|
- Ideal para: Procedimientos, normas, conceptos
|
||||||
|
|
||||||
|
**Video:**
|
||||||
|
- Videos cortos (2-5 min)
|
||||||
|
- Player embebido (YouTube/Vimeo o upload)
|
||||||
|
- Ideal para: Demostraciones, operaciones de equipo
|
||||||
|
|
||||||
|
**¿Por qué microlearning?**
|
||||||
|
- Empleados realizan en el intervalo (10-15min)
|
||||||
|
- Contenido corto = mayor retención
|
||||||
|
- Facilita actualización (vs cursos largos)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Sistema de Administración Delegada
|
||||||
|
|
||||||
|
**Jerarquía:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Admin General (RRHH)
|
||||||
|
↓ puede promover
|
||||||
|
Admin de Área (Gerentes)
|
||||||
|
↓ puede visualizar solo
|
||||||
|
Empleados de su área
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permisos:**
|
||||||
|
- Admin general: Crea rutas, promueve admins, ve todos los datos
|
||||||
|
- Admin de área: Ve solo reportes de su área
|
||||||
|
- Empleado: Solo realiza capacitaciones
|
||||||
|
|
||||||
|
**Auditoría:**
|
||||||
|
- Logs de quién creó/editó cada contenido
|
||||||
|
- Histórico de promociones a admin
|
||||||
|
- Compliance SOX/ISO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Exportación para Compliance
|
||||||
|
|
||||||
|
**Formato del Excel generado:**
|
||||||
|
|
||||||
|
| Matrícula | Nombre | Área | Ruta | Fecha Conclusión | Puntos | Estado |
|
||||||
|
|-----------|------|------|--------|----------------|--------|--------|
|
||||||
|
| 1001 | João Silva | Producción | Seguridad NR-20 | 15/11/2024 | 95/100 | ✅ Aprobado |
|
||||||
|
| 1002 | María Santos | Logística | Manejo Gases | 14/11/2024 | 78/100 | ✅ Aprobado |
|
||||||
|
|
||||||
|
**Pestaña adicional: Detalle de Respuestas**
|
||||||
|
- Permite auditoría: "¿Empleado X acertó pregunta Y?"
|
||||||
|
- Evidencia para procesos laborales
|
||||||
|
- Compliance NR-13/NR-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultados e Impacto
|
||||||
|
|
||||||
|
### Sistema en Producción
|
||||||
|
|
||||||
|
**Estado actual:** En uso hace 4+ meses
|
||||||
|
|
||||||
|
**Métricas de adopción:**
|
||||||
|
- 👥 500+ empleados registrados
|
||||||
|
- 📚 12 rutas activas
|
||||||
|
- 📖 150+ microlearnings creados
|
||||||
|
- ✅ 8.000+ capacitaciones completadas
|
||||||
|
- 📊 100+ reportes exportados (compliance)
|
||||||
|
|
||||||
|
**Tasa de conclusión:** 87% (media industria: 45%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto en el Cliente
|
||||||
|
|
||||||
|
**Antes:**
|
||||||
|
- Capacitaciones presenciales (costo alto, agenda difícil)
|
||||||
|
- Evidencias en papel (pérdidas, difícil auditoría)
|
||||||
|
- Dificultad en actualizar contenido
|
||||||
|
|
||||||
|
**Después:**
|
||||||
|
- Capacitación asíncrona (empleado realiza cuando puede)
|
||||||
|
- Evidencias digitales (compliance facilitado)
|
||||||
|
- RRHH actualiza contenido sin llamar a TI
|
||||||
|
- Reducción del 70% en costo de capacitación
|
||||||
|
|
||||||
|
**Feedback del cliente:**
|
||||||
|
> "Si hubiéramos implementado solo lo que pedimos inicialmente, el sistema sería inútil. El análisis previo salvó el proyecto."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Valor del Solution Design
|
||||||
|
|
||||||
|
**ROI del análisis preventa:**
|
||||||
|
|
||||||
|
**Escenario A (sin análisis):**
|
||||||
|
1. Desarrollar solo interfaz (2 meses)
|
||||||
|
2. Cliente prueba y percibe que falta CRUD (1 mes después)
|
||||||
|
3. Refacción para agregar módulos (2+ meses)
|
||||||
|
4. **Total: 5+ meses + frustración del cliente**
|
||||||
|
|
||||||
|
**Escenario B (con análisis - lo que hicimos):**
|
||||||
|
1. Identificar requisitos antes (1 semana)
|
||||||
|
2. Aprobar alcance completo (1 semana)
|
||||||
|
3. Desarrollar solución correcta (4 meses)
|
||||||
|
4. **Total: 4 meses + cliente satisfecho**
|
||||||
|
|
||||||
|
**Economía:** 1+ mes de retrabajo + costo de oportunidad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`.NET 7` `C#` `Entity Framework Core` `SQL Server` `React` `Material-UI` `Azure App Service` `Azure Blob Storage` `ClosedXML` `Chart.js` `User Stories` `Solution Design` `Tech Lead`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisiones Clave & Trade-offs
|
||||||
|
|
||||||
|
### ¿Por qué no usar LMS listo? (Moodle, Canvas)
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
1. ❌ Moodle (open-source, gratuito)
|
||||||
|
2. ❌ Totara/Canvas (LMS corporativo)
|
||||||
|
3. ✅ **Desarrollo custom**
|
||||||
|
|
||||||
|
**Justificación:**
|
||||||
|
- LMS genérico: Complejidad innecesaria (foros, wikis, etc)
|
||||||
|
- Cliente quiere **solo microlearning** (simplicidad)
|
||||||
|
- Costo de licencia LMS > costo de dev custom
|
||||||
|
- Integración con AD/SSO del cliente (más fácil custom)
|
||||||
|
- UX optimizada para planta (mobile-first, touch)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ¿Por qué 3 opciones de respuesta (vs 4-5)?
|
||||||
|
|
||||||
|
**Elección:** Verde (Excelente), Amarillo (Regular), Rojo (Malo)
|
||||||
|
|
||||||
|
**Justificación:**
|
||||||
|
- Empleados de planta prefieren simplicidad
|
||||||
|
- Colores universales (semáforo)
|
||||||
|
- Evita paradoja de la elección (menos opciones = más engagement)
|
||||||
|
- Gamificación más clara
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ¿Por qué Export Excel (vs Dashboard online)?
|
||||||
|
|
||||||
|
**Ambos fueron implementados**, pero Excel es crítico para:
|
||||||
|
|
||||||
|
**Compliance regulatorio:**
|
||||||
|
- Auditores piden "archivo firmado digitalmente"
|
||||||
|
- NR-13/NR-20 exigen evidencia física
|
||||||
|
- Procesos laborales aceptan Excel
|
||||||
|
|
||||||
|
**Flexibilidad:**
|
||||||
|
- RRHH puede hacer análisis personalizados en Excel
|
||||||
|
- Combinar con otras fuentes de datos
|
||||||
|
- Presentaciones para dirección
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecciones Aprendidas
|
||||||
|
|
||||||
|
### 1. Solution Design Previene Retrabajo
|
||||||
|
|
||||||
|
**Lección:** 1 semana de análisis crítico economiza meses de refacción.
|
||||||
|
|
||||||
|
**Aplicación:**
|
||||||
|
- Siempre cuestionar especificaciones incompletas
|
||||||
|
- Pensar en el "día siguiente" (¿quién gestiona esto en producción?)
|
||||||
|
- Involucrar cliente en discusiones de requisitos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. UX ≠ Requisitos Funcionales
|
||||||
|
|
||||||
|
**Lección:** Wireframes bonitos no sustituyen análisis de requisitos.
|
||||||
|
|
||||||
|
**UX se enfoca en:** Cómo usuario **usa** el sistema
|
||||||
|
**Solution Design se enfoca en:** Cómo sistema **funciona** end-to-end
|
||||||
|
|
||||||
|
Ambos son necesarios y complementarios.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Preguntar "¿Cómo?" es Más Importante que "¿Qué?"
|
||||||
|
|
||||||
|
**Cliente dice:** "Quiero rutas y microlearnings"
|
||||||
|
**Solution Designer pregunta:** "¿Cómo entra la primera ruta al sistema?"
|
||||||
|
|
||||||
|
Esta pregunta simple reveló 4 módulos faltantes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. User Stories Bien Escritas Aceleran Desarrollo
|
||||||
|
|
||||||
|
**Inversión:** 2 semanas escribiendo 32 user stories detalladas
|
||||||
|
|
||||||
|
**Retorno:**
|
||||||
|
- Devs sabían exactamente qué construir
|
||||||
|
- Zero ambigüedad
|
||||||
|
- Muy pocos bugs (requisitos claros)
|
||||||
|
- Cliente validó historias antes de codificar
|
||||||
|
|
||||||
|
**Lección:** Tiempo gastado en planificación reduce tiempo de desarrollo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Compliance es Requisito Oculto
|
||||||
|
|
||||||
|
**En industrias reguladas** (salud, energía, químico), siempre habrá:
|
||||||
|
- Necesidad de auditoría
|
||||||
|
- Exportación de evidencias
|
||||||
|
- Logs de quién hizo qué
|
||||||
|
|
||||||
|
**Lección:** Preguntar sobre compliance **antes**, no después.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafíos Superados
|
||||||
|
|
||||||
|
| Desafío | Solución | Resultado |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| Especificación incompleta | Análisis crítico preventa | Alcance correcto desde inicio |
|
||||||
|
| Cliente sin conocimiento técnico | User stories en lenguaje de negocio | Cliente validó requisitos |
|
||||||
|
| Empleados con baja familiaridad digital | UX simplificado (3 botones, colores) | 87% tasa de conclusión |
|
||||||
|
| Compliance NR-13/NR-20 | Export Excel con detalle | Aprobado en 2 auditorías |
|
||||||
|
| Gestión de 500+ usuarios | Import CSV + jerarquía de admins | Onboarding en 1 semana |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Próximos Pasos (Roadmap Futuro)
|
||||||
|
|
||||||
|
**Funcionalidades planificadas:**
|
||||||
|
|
||||||
|
1. **Notificaciones Push**
|
||||||
|
- Recordar empleado de capacitación pendiente
|
||||||
|
- Avisar de nueva ruta obligatoria
|
||||||
|
|
||||||
|
2. **App Mobile Nativo**
|
||||||
|
- Offline-first (videos descargados)
|
||||||
|
- Empleados sin computadora
|
||||||
|
|
||||||
|
3. **Certificados Digitales**
|
||||||
|
- PDF firmado digitalmente
|
||||||
|
- QR code para validación
|
||||||
|
|
||||||
|
4. **Inteligencia de Datos**
|
||||||
|
- ¿Qué microlearnings tienen más error?
|
||||||
|
- Identificar gaps de conocimiento por área
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** Sistema funcional en producción, cliente satisfecho, zero retrabajo - todo porque 1 semana fue invertida en **pensar antes de codificar**.
|
||||||
|
|
||||||
|
[¿Necesita análisis crítico de requisitos? Póngase en contacto](#contact)
|
||||||
577
Content/Cases/es/pharma-digital-transformation.md
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
---
|
||||||
|
title: "MVP Digital para Laboratorio Farmacéutico - De Cero a Producción"
|
||||||
|
slug: "pharma-digital-transformation"
|
||||||
|
summary: "Liderazgo de squad en proyecto greenfield para laboratorio farmacéutico, construyendo MVP de plataforma digital con integraciones complejas (Salesforce, Twilio, APIs oficiales) partiendo de cero absoluto - sin Git, sin servidores, sin infraestructura."
|
||||||
|
client: "Laboratorio Farmacéutico"
|
||||||
|
industry: "Farmacéutica & Salud"
|
||||||
|
timeline: "4 meses (2 meses de retraso planificado)"
|
||||||
|
role: "Tech Lead & Solution Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- MVP
|
||||||
|
- Digital Transformation
|
||||||
|
- .NET
|
||||||
|
- React
|
||||||
|
- Next.js
|
||||||
|
- Salesforce
|
||||||
|
- Twilio
|
||||||
|
- SQL Server
|
||||||
|
- Tech Lead
|
||||||
|
- Greenfield
|
||||||
|
featured: true
|
||||||
|
order: 3
|
||||||
|
date: 2023-03-01
|
||||||
|
seo_title: "MVP Digital Farmacéutico - Transformación Digital de Cero"
|
||||||
|
seo_description: "Caso de construcción de MVP digital para laboratorio farmacéutico partiendo de cero: sin Git, sin infraestructura, con integraciones complejas y entrega exitosa."
|
||||||
|
seo_keywords: "MVP, digital transformation, pharma, .NET, React, Next.js, Salesforce, greenfield project, tech lead"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
Laboratorio farmacéutico en el **inicio de transformación digital** contrata consultoría para construir plataforma de descuentos para médicos prescriptores, partiendo de prototipo en WordPress.
|
||||||
|
|
||||||
|
**Desafío único:** Comenzar proyecto greenfield en empresa **sin infraestructura básica** de desarrollo - sin Git, sin servidores aprovisionados, sin procesos definidos.
|
||||||
|
|
||||||
|
**Contexto:** Proyecto ejecutado en ambiente de múltiples squads. **Entrega exitosa en producción** a pesar de los desafíos iniciales de infraestructura, con retraso controlado de 2 meses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafío
|
||||||
|
|
||||||
|
### Transformación Digital... Partiendo de Cero Absoluto
|
||||||
|
|
||||||
|
**Estado inicial de la empresa (2023):**
|
||||||
|
|
||||||
|
❌ **Sin Git/versionamiento**
|
||||||
|
- Código solo en máquinas locales
|
||||||
|
- Histórico inexistente
|
||||||
|
- Colaboración imposible
|
||||||
|
|
||||||
|
❌ **Sin servidores aprovisionados**
|
||||||
|
- Ambiente de desarrollo inexistente
|
||||||
|
- Homologación no configurada
|
||||||
|
- Producción no preparada
|
||||||
|
|
||||||
|
❌ **Sin procesos de desarrollo**
|
||||||
|
- Sin CI/CD
|
||||||
|
- Sin code review
|
||||||
|
- Sin gestión de tareas estructurada
|
||||||
|
|
||||||
|
❌ **Sin equipo técnico interno experimentado**
|
||||||
|
- Equipo sin familiaridad con stacks modernos
|
||||||
|
- Primer contacto con React, APIs REST
|
||||||
|
- Inexperiencia con integraciones complejas
|
||||||
|
|
||||||
|
**Punto de partida técnico:**
|
||||||
|
- Prototipo funcional en **WordPress**
|
||||||
|
- Contenido y textos ya aprobados
|
||||||
|
- UX/UI definido
|
||||||
|
- Reglas de negocio documentadas (parcialmente)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Integraciones Complejas Requeridas
|
||||||
|
|
||||||
|
El MVP necesitaba integrar con múltiples sistemas externos:
|
||||||
|
|
||||||
|
1. 🔐 **Salesforce** - Registro de pedidos de descuento
|
||||||
|
2. 📱 **Twilio** - SMS para validación de login (2FA)
|
||||||
|
3. 🏥 **API oficial de médicos** - Validación de CRM + datos profesionales
|
||||||
|
4. 🎯 **Interplayers** - Envío de registros de descuento por CPF
|
||||||
|
5. 📄 **WordPress** - Lectura de contenido (CMS headless)
|
||||||
|
6. 💾 **SQL Server** - Persistencia de datos
|
||||||
|
|
||||||
|
**Complejidad adicional:**
|
||||||
|
- Diferentes credenciales/ambientes por integración
|
||||||
|
- SLAs variados (Twilio crítico, WordPress tolerante)
|
||||||
|
- Tratamiento de errores específico por provider
|
||||||
|
- Compliance LGPD (datos sensibles de médicos)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquitectura de Solución
|
||||||
|
|
||||||
|
### Estrategia: Start Small, Build Solid
|
||||||
|
|
||||||
|
**Decisión inicial:** Explicar al equipo el proceso que seguiríamos, estableciendo fundaciones antes de codificar.
|
||||||
|
|
||||||
|
### Fase 1: Setup de Infraestructura Básica (Semanas 1-2)
|
||||||
|
|
||||||
|
Incluso sin servidores aprovisionados, inicié setup esencial:
|
||||||
|
|
||||||
|
**Git & Versionamiento:**
|
||||||
|
```bash
|
||||||
|
# Repositorio estructurado desde día 1
|
||||||
|
git init
|
||||||
|
git flow init # Branch strategy definida
|
||||||
|
|
||||||
|
# Estructura de monorepo
|
||||||
|
/
|
||||||
|
├── frontend/ # Next.js + React
|
||||||
|
├── backend/ # .NET APIs
|
||||||
|
├── cms-adapter/ # WordPress integration
|
||||||
|
└── docs/ # Arquitectura y ADRs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Proceso explicado al equipo:**
|
||||||
|
1. ✅ Todo en Git (commits atómicos, mensajes descriptivos)
|
||||||
|
2. ✅ Feature branches (nunca commit directo en main)
|
||||||
|
3. ✅ Code review obligatorio (2 aprobaciones)
|
||||||
|
4. ✅ CI/CD preparado (para cuando servidores estén listos)
|
||||||
|
|
||||||
|
**Ambientes locales primero:**
|
||||||
|
- Docker Compose para desarrollo local
|
||||||
|
- Mock de APIs externas (hasta que lleguen credenciales)
|
||||||
|
- SQL Server local con seeds de datos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Arquitectura Moderna & Desacoplada
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ FRONTEND (Next.js + React) │
|
||||||
|
│ - SSR para SEO │
|
||||||
|
│ - Client-side para interactividad │
|
||||||
|
│ - Consumo de APIs │
|
||||||
|
└────────────┬────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ BACKEND APIs (.NET 7) │
|
||||||
|
│ - REST APIs │
|
||||||
|
│ - Authentication/Authorization │
|
||||||
|
│ - Business logic │
|
||||||
|
│ - Orchestration layer │
|
||||||
|
└────┬────┬────┬────┬────┬─────────────────────────┬──┘
|
||||||
|
│ │ │ │ │ │
|
||||||
|
▼ ▼ ▼ ▼ ▼ ▼
|
||||||
|
┌────────┐ ┌──────┐ ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
|
||||||
|
│Salesf. │ │Twilio│ │CRM │ │Interpl.│ │WordPr. │ │SQL Server│
|
||||||
|
│ │ │ │ │API │ │ │ │(CMS) │ │ │
|
||||||
|
└────────┘ └──────┘ └──────┘ └────────┘ └────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stack elegido:**
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `Next.js 13` - SSR, routing, optimizaciones
|
||||||
|
- `React 18` - Componentes, hooks, context
|
||||||
|
- `TypeScript` - Type safety
|
||||||
|
- `Tailwind CSS` - Styling moderno
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `.NET 7` - APIs REST
|
||||||
|
- `Entity Framework Core` - ORM
|
||||||
|
- `SQL Server 2019` - Base de datos
|
||||||
|
- `Polly` - Resilience patterns (retry, circuit breaker)
|
||||||
|
|
||||||
|
**¿Por qué Next.js en vez de mantener WordPress?**
|
||||||
|
- ✅ Performance (SSR vs PHP monolítico)
|
||||||
|
- ✅ SEO optimizado (crítico para farmacéutica)
|
||||||
|
- ✅ Experiencia moderna (SPA cuando necesario)
|
||||||
|
- ✅ Escalabilidad
|
||||||
|
- ✅ WordPress mantenido solo como CMS (headless)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Integraciones (Core del Proyecto)
|
||||||
|
|
||||||
|
#### 1. Salesforce - Campañas y Registro de Pedidos
|
||||||
|
|
||||||
|
**Solución implementada:**
|
||||||
|
|
||||||
|
Salesforce fue configurado para gestionar dos funcionalidades principales:
|
||||||
|
|
||||||
|
**a) Campañas de descuento:**
|
||||||
|
- Marketing configura campañas en Salesforce (medicamento X, descuento Y%, período)
|
||||||
|
- Backend consulta campañas activas vía API
|
||||||
|
- Frontend (Next.js) muestra porcentaje de descuento disponible basado en: medicamento + campaña activa
|
||||||
|
|
||||||
|
**b) Registro de pedidos:**
|
||||||
|
- Usuario informa: CRM del médico, UF, CPF del paciente, medicamento
|
||||||
|
- Sistema valida datos (CRM real vía API oficial, CPF válido)
|
||||||
|
- Porcentaje es calculado automáticamente (campañas de Salesforce + reglas del CMS)
|
||||||
|
- Pedido es registrado en Salesforce con todos los datos (compliance LGPD)
|
||||||
|
|
||||||
|
**Desafíos técnicos superados:**
|
||||||
|
- Autenticación OAuth2 con refresh token automático
|
||||||
|
- Rate limiting (Salesforce tiene límites de API/día)
|
||||||
|
- Retry logic para fallas transitorias (Polly)
|
||||||
|
- Enmascaramiento de CPF para logs (LGPD)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Twilio - Autenticación por SMS (2FA)
|
||||||
|
|
||||||
|
**Solución implementada:**
|
||||||
|
|
||||||
|
Sistema de autenticación de dos factores para garantizar seguridad:
|
||||||
|
|
||||||
|
**Flujo de login:**
|
||||||
|
1. Usuario informa teléfono
|
||||||
|
2. Backend genera código de 6 dígitos (válido por 5 minutos)
|
||||||
|
3. SMS enviado vía Twilio ("Su código: 123456")
|
||||||
|
4. Usuario digita código en frontend
|
||||||
|
5. Backend valida código + timestamp de expiración
|
||||||
|
6. Token JWT emitido tras validación exitosa
|
||||||
|
|
||||||
|
**Compliance y auditoría:**
|
||||||
|
- Teléfonos enmascarados en logs (LGPD)
|
||||||
|
- Auditoría completa (quién, cuándo, qué SMS)
|
||||||
|
- Tasa de entrega: 99.8%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. API Oficial de Médicos (Consejo Regional de Medicina)
|
||||||
|
|
||||||
|
**Solución implementada:**
|
||||||
|
|
||||||
|
Validación automática de médicos vía API oficial de los consejos de medicina:
|
||||||
|
|
||||||
|
**Validaciones realizadas:**
|
||||||
|
- CRM existe y está activo en el consejo
|
||||||
|
- Nombre del médico corresponde al CRM informado
|
||||||
|
- Especialidad es permitida (regla de negocio del laboratorio)
|
||||||
|
- UF corresponde al estado de registro
|
||||||
|
|
||||||
|
**Optimizaciones:**
|
||||||
|
- Cache de 24 horas para reducir llamadas a API oficial
|
||||||
|
- Fallback en caso de API fuera del aire (notifica admin)
|
||||||
|
- Retry automático para fallas transitorias
|
||||||
|
|
||||||
|
**Por qué esto importa:**
|
||||||
|
Garantiza que solo médicos reales y activos puedan prescribir descuentos, evitando fraudes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. WordPress como CMS Headless
|
||||||
|
|
||||||
|
**Solución implementada:**
|
||||||
|
|
||||||
|
Marketing continúa gestionando contenido en WordPress (familiar), pero frontend es Next.js moderno.
|
||||||
|
|
||||||
|
**Arquitectura:**
|
||||||
|
- WordPress: Gestiona textos, imágenes, reglas de campañas
|
||||||
|
- WordPress REST API: Expone contenido vía JSON
|
||||||
|
- Next.js: Consume API y renderiza con SSR (SEO optimizado)
|
||||||
|
|
||||||
|
**Beneficios:**
|
||||||
|
- ✅ Marketing no necesita aprender nueva herramienta
|
||||||
|
- ✅ Frontend moderno (performance, UX)
|
||||||
|
- ✅ SEO optimizado (Server-Side Rendering)
|
||||||
|
- ✅ Separación clara de responsabilidades (contenido vs código)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4: Resiliencia & Error Handling
|
||||||
|
|
||||||
|
Con múltiples integraciones externas, fallas son inevitables. La solución fue implementar **patrones de resiliencia** usando biblioteca Polly (.NET):
|
||||||
|
|
||||||
|
**Patrones implementados:**
|
||||||
|
|
||||||
|
**1. Retry (Reintentar)**
|
||||||
|
- Si Salesforce/Twilio/CRM API fallan, sistema intenta automáticamente 2-3x
|
||||||
|
- Espera crece exponencialmente (1s, 2s, 4s) para evitar sobrecarga
|
||||||
|
- Solo errores transitorios (timeout, 503) son reintentados
|
||||||
|
|
||||||
|
**2. Circuit Breaker (Disyuntor)**
|
||||||
|
- Si servicio falla 5x seguidas, "abre el circuito" por 30s
|
||||||
|
- Durante 30s, no intenta más (evita desperdiciar recursos)
|
||||||
|
- Tras 30s, intenta nuevamente (puede haber vuelto)
|
||||||
|
|
||||||
|
**3. Timeout**
|
||||||
|
- Cada integración tiene tiempo máximo de respuesta
|
||||||
|
- Evita requisiciones trabadas indefinidamente
|
||||||
|
|
||||||
|
**4. Fallback (Plan B)**
|
||||||
|
- Salesforce fuera: Pedido va a cola, procesa después
|
||||||
|
- Twilio fuera: Alerta administrador vía email
|
||||||
|
- CRM API fuera: Usa cache (datos de 24h atrás)
|
||||||
|
- WordPress fuera: Muestra contenido estático precargado
|
||||||
|
|
||||||
|
**Estrategias por integración:**
|
||||||
|
|
||||||
|
| Integración | Retry | Circuit Breaker | Timeout | Plan B |
|
||||||
|
|----------|-------|-----------------|---------|----------|
|
||||||
|
| Salesforce | 3x (exponencial) | 5 fallas/30s | 10s | Cola de retry |
|
||||||
|
| Twilio | 2x (lineal) | 3 fallas/60s | 5s | Alerta admin |
|
||||||
|
| CRM API | 3x (exponencial) | No | 15s | Cache |
|
||||||
|
| WordPress | No | No | 3s | Contenido estático |
|
||||||
|
|
||||||
|
**Resultado en producción:**
|
||||||
|
- Salesforce tuvo mantenimiento (1h) → Sistema continuó funcionando (cola procesó después)
|
||||||
|
- Twilio tuvo inestabilidad → Retry automático resolvió 95% de los casos
|
||||||
|
- Zero downtime percibido por los usuarios
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Superando Desafíos de Infraestructura
|
||||||
|
|
||||||
|
### Problema: Servidores No Aprovisionados
|
||||||
|
|
||||||
|
**Solución temporal:**
|
||||||
|
1. Desarrollo 100% local (Docker Compose)
|
||||||
|
2. Mocks de servicios externos (cuando credenciales se retrasaron)
|
||||||
|
3. CI/CD preparado pero no activo (esperando infra)
|
||||||
|
|
||||||
|
**Cuando llegaron servidores (semana 6):**
|
||||||
|
- Deploy en 2 horas (ya estaba preparado)
|
||||||
|
- Zero sorpresas (todo probado localmente)
|
||||||
|
- Rollout suave
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problema: Credenciales de Integración Retrasadas
|
||||||
|
|
||||||
|
**Impacto:** Twilio y Salesforce demoraron 3 semanas en ser aprovisionadas.
|
||||||
|
|
||||||
|
**Solución:** Crear versiones "mock" (simuladas) de cada integración:
|
||||||
|
- Mock de Twilio: Registra en log en vez de enviar SMS real
|
||||||
|
- Mock de Salesforce: Guarda pedido en archivo JSON local
|
||||||
|
- Mock de CRM API: Retorna datos ficticios de médicos
|
||||||
|
|
||||||
|
**Cómo funciona:**
|
||||||
|
- Ambiente de desarrollo: Usa mocks (no necesita credenciales)
|
||||||
|
- Ambiente de producción: Usa integraciones reales (cuando lleguen credenciales)
|
||||||
|
- Cambio automático basado en configuración
|
||||||
|
|
||||||
|
**Resultado:** Equipo continuó 100% productivo durante 3 semanas, probando flujos completos sin depender de credenciales.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problema: Equipo Inexperto con Stack Moderno
|
||||||
|
|
||||||
|
**Contexto:** Equipo no tenía experiencia con React, TypeScript, .NET Core moderno, APIs REST.
|
||||||
|
|
||||||
|
**Abordaje de capacitación:**
|
||||||
|
|
||||||
|
**1. Pair Programming (1h/día por desarrollador)**
|
||||||
|
- Tech lead trabaja al lado del dev
|
||||||
|
- Compartir pantalla + explicación en tiempo real
|
||||||
|
- Dev escribe código, tech lead guía
|
||||||
|
|
||||||
|
**2. Code Review Educativo**
|
||||||
|
- No solo "aprobar" o "reprobar"
|
||||||
|
- Comentarios explican el **por qué** de cada sugerencia
|
||||||
|
- Ejemplo: "Siempre trate errores de requisiciones! Si API cae, usuario necesita saber qué pasó."
|
||||||
|
|
||||||
|
**3. Documentación Viva**
|
||||||
|
- ADRs (Architecture Decision Records): ¿Por qué elegimos X y no Y?
|
||||||
|
- READMEs: Cómo ejecutar, cómo probar, cómo deployar
|
||||||
|
- Onboarding guide: De cero a primera feature
|
||||||
|
|
||||||
|
**4. Live Coding Semanal (2h)**
|
||||||
|
- Tech lead resuelve problema real en vivo
|
||||||
|
- Equipo observa proceso de pensamiento
|
||||||
|
- Q&A al final
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
- Tras 4 semanas, equipo estaba autónomo
|
||||||
|
- Calidad de código aumentó consistentemente
|
||||||
|
- Devs pasaron a hacer code review entre sí (peer review)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultados e Impacto
|
||||||
|
|
||||||
|
### Entrega Exitosa A Pesar de los Desafíos
|
||||||
|
|
||||||
|
**Contexto:** Programa con múltiples squads trabajando en paralelo.
|
||||||
|
|
||||||
|
**Resultado alcanzado:**
|
||||||
|
- ✅ **MVP entregado en producción con éxito**
|
||||||
|
- ✅ Retraso controlado de 2 meses (significativamente menor que otras iniciativas del programa)
|
||||||
|
- ✅ Todas las integraciones funcionando según planificado
|
||||||
|
- ✅ Zero critical bugs en producción (primera semana)
|
||||||
|
|
||||||
|
**¿Por qué la entrega fue exitosa?**
|
||||||
|
|
||||||
|
1. **Setup anticipado** - Git, procesos, Docker local desde día 1
|
||||||
|
2. **Mocks estratégicos** - Equipo no quedó bloqueado esperando infra
|
||||||
|
3. **Arquitectura sólida** - Resiliencia desde el inicio
|
||||||
|
4. **Upskilling continuo** - Equipo aprendió haciendo
|
||||||
|
5. **Comunicación proactiva** - Riesgos reportados temprano
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Métricas del MVP
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- ⚡ Tiempo de carga: <2s (95th percentile)
|
||||||
|
- 📱 Lighthouse score: 95+ (mobile)
|
||||||
|
- 🔒 SSL A+ rating
|
||||||
|
|
||||||
|
**Integraciones:**
|
||||||
|
- 📊 Salesforce: 100% de pedidos sincronizados
|
||||||
|
- 📱 Twilio: 99.8% delivery rate
|
||||||
|
- 🏥 CRM API: 10k validaciones/día (media)
|
||||||
|
- 💾 SQL Server: 50k registros/mes
|
||||||
|
|
||||||
|
**Adopción:**
|
||||||
|
- 👨⚕️ 2.000+ médicos registrados (3 primeros meses)
|
||||||
|
- 🎯 15.000+ pedidos de descuento procesados
|
||||||
|
- ⭐ 4.8/5 satisfacción (encuesta interna)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto en el Cliente
|
||||||
|
|
||||||
|
**Transformación digital iniciada:**
|
||||||
|
- ✅ Git implementado y adoptado
|
||||||
|
- ✅ Procesos de desarrollo establecidos
|
||||||
|
- ✅ Equipo interno capacitado en stacks modernos
|
||||||
|
- ✅ Infraestructura cloud configurada (Azure)
|
||||||
|
- ✅ Roadmap de evolución definido
|
||||||
|
|
||||||
|
**Base para futuros proyectos:**
|
||||||
|
- Arquitectura sirvió de referencia para otras iniciativas
|
||||||
|
- Patrones de código documentados (coding standards)
|
||||||
|
- Pipelines CI/CD reutilizados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`.NET 7` `C#` `Entity Framework Core` `SQL Server` `React 18` `Next.js 13` `TypeScript` `Tailwind CSS` `Salesforce API` `Twilio` `WordPress REST API` `Docker` `Polly` `OAuth2` `JWT` `LGPD Compliance`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisiones Clave & Trade-offs
|
||||||
|
|
||||||
|
### ¿Por qué Next.js en vez de React puro?
|
||||||
|
|
||||||
|
**Requisitos:**
|
||||||
|
- SEO crítico (farmacéutica necesita ranquear)
|
||||||
|
- Performance (médicos usan mobile)
|
||||||
|
- Contenido dinámico (WordPress)
|
||||||
|
|
||||||
|
**Next.js ofrece:**
|
||||||
|
- ✅ SSR out-of-the-box
|
||||||
|
- ✅ API routes (BFF pattern)
|
||||||
|
- ✅ Optimizaciones automáticas (image, fonts)
|
||||||
|
- ✅ Deploy simplificado (Vercel, Azure)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ¿Por qué mantener WordPress?
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
1. ❌ Migrar contenido a BD + CMS custom (tiempo)
|
||||||
|
2. ❌ Strapi/Contentful (costos + learning curve)
|
||||||
|
3. ✅ **WordPress headless** (mejor trade-off)
|
||||||
|
|
||||||
|
**Ventajas:**
|
||||||
|
- Equipo de marketing ya sabe usar
|
||||||
|
- Contenido aprobado ya estaba ahí
|
||||||
|
- WordPress REST API es sólida
|
||||||
|
- Costo cero (ya estaba ejecutándose)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ¿Por qué .NET 7 en vez de Node.js?
|
||||||
|
|
||||||
|
**Contexto:** Cliente tenía preferencia por stack Microsoft.
|
||||||
|
|
||||||
|
**Beneficios adicionales:**
|
||||||
|
- Performance superior (vs Node en APIs)
|
||||||
|
- Type safety nativa (C#)
|
||||||
|
- Entity Framework (ORM maduro)
|
||||||
|
- Integración fácil con Azure (deploy futuro)
|
||||||
|
- Equipo del cliente tenía familiaridad
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lecciones Aprendidas
|
||||||
|
|
||||||
|
### 1. Infraestructura Retrasada? Prepare Alternativas
|
||||||
|
|
||||||
|
No espere servidores/credenciales para comenzar:
|
||||||
|
- Docker local es su amigo
|
||||||
|
- Mocks permiten progreso
|
||||||
|
- CI/CD puede ser preparado antes de tener dónde deployar
|
||||||
|
|
||||||
|
**Lección:** Controle lo que puede controlar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Procesos > Herramientas
|
||||||
|
|
||||||
|
Incluso sin Git corporativo, establecí:
|
||||||
|
- Branching strategy
|
||||||
|
- Code review
|
||||||
|
- Commit conventions
|
||||||
|
- Documentation standards
|
||||||
|
|
||||||
|
**Resultado:** Cuando llegaron herramientas, equipo ya sabía usarlas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Upskilling es Inversión, No Costo
|
||||||
|
|
||||||
|
Pair programming y code reviews tomaron tiempo, pero:
|
||||||
|
- ✅ Equipo quedó autónomo más rápido
|
||||||
|
- ✅ Calidad de código aumentó
|
||||||
|
- ✅ Knowledge sharing natural
|
||||||
|
- ✅ Onboarding de nuevos devs simplificado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Resiliencia Desde el Inicio
|
||||||
|
|
||||||
|
Implementar Polly (retry, circuit breaker) al inicio salvó en producción:
|
||||||
|
- Twilio tuvo inestabilidad (resuelta automáticamente)
|
||||||
|
- Salesforce tuvo mantenimiento (queue funcionó)
|
||||||
|
- CRM API tuvo lentitud (cache mitigó)
|
||||||
|
|
||||||
|
**Lección:** No deje resiliencia para "después". Fallas van a ocurrir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Comunicación Clara de Riesgos
|
||||||
|
|
||||||
|
Reporté semanalmente:
|
||||||
|
- Bloqueos (infraestructura, credenciales)
|
||||||
|
- Riesgos (plazos, dependencias)
|
||||||
|
- Soluciones alternativas (mocks, workarounds)
|
||||||
|
|
||||||
|
**Resultado:** Stakeholders sabían exactamente el estado y no tuvieron sorpresas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafíos & Cómo Fueron Superados
|
||||||
|
|
||||||
|
| Desafío | Impacto | Solución | Resultado |
|
||||||
|
|---------|---------|---------|-----------|
|
||||||
|
| Sin Git | Bloqueo total | Setup local + GitLab Cloud | Equipo productivo día 1 |
|
||||||
|
| Sin servidores | Sin ambiente de dev | Docker Compose local | Dev/test local completo |
|
||||||
|
| Credenciales retrasadas | Integración bloqueada | Mock services | Progreso sin bloqueo |
|
||||||
|
| Equipo inexperto | Código de baja calidad | Pair prog + Code review | Ramp-up en 4 semanas |
|
||||||
|
| Múltiples integraciones | Complejidad alta | Polly + patterns | Zero downtime prod |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Próximos Pasos (Post-MVP)
|
||||||
|
|
||||||
|
**Roadmap sugerido al cliente:**
|
||||||
|
|
||||||
|
1. **Fase 2: Expansión de funcionalidades**
|
||||||
|
- Dashboard para médicos (histórico de pedidos)
|
||||||
|
- Notificaciones push (Firebase)
|
||||||
|
- Integración con e-commerce (compra directa)
|
||||||
|
|
||||||
|
2. **Fase 3: Optimizaciones**
|
||||||
|
- Cache distribuido (Redis)
|
||||||
|
- CDN para assets estáticos
|
||||||
|
- Analytics avanzado (Amplitude)
|
||||||
|
|
||||||
|
3. **Fase 4: Escala**
|
||||||
|
- Kubernetes (AKS)
|
||||||
|
- Microservicios (quebrar monolito)
|
||||||
|
- Event-driven architecture (Azure Service Bus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** MVP entregado en producción a pesar de comenzar literalmente de cero, estableciendo fundaciones sólidas para transformación digital del cliente.
|
||||||
|
|
||||||
|
[¿Necesita construir un MVP en escenario desafiante? Póngase en contacto](#contact)
|
||||||
211
Content/Cases/es/sap-integration-healthcare.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
title: "Sistema de Integración SAP Healthcare"
|
||||||
|
slug: "sap-integration-healthcare"
|
||||||
|
summary: "Integración bidireccional procesando 100k+ transacciones/día con 99.9% uptime"
|
||||||
|
client: "Confidencial - Multinacional Healthcare"
|
||||||
|
industry: "Healthcare"
|
||||||
|
timeline: "6 meses"
|
||||||
|
role: "Arquitecto de Integración"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- SAP
|
||||||
|
- C#
|
||||||
|
- .NET
|
||||||
|
- Integraciones
|
||||||
|
- Enterprise
|
||||||
|
- Healthcare
|
||||||
|
featured: true
|
||||||
|
order: 1
|
||||||
|
date: 2023-06-15
|
||||||
|
seo_title: "Caso: Integración SAP Healthcare - 100k Transacciones/Día"
|
||||||
|
seo_description: "Cómo arquitectamos sistema de integración SAP procesando 100k+ transacciones diarias con 99.9% uptime para empresa healthcare."
|
||||||
|
seo_keywords: "integración SAP, C#, .NET, SAP Connector, enterprise integration, healthcare"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Descripción General
|
||||||
|
|
||||||
|
**Cliente:** Multinacional Healthcare (confidencial)
|
||||||
|
**Tamaño:** 100.000+ empleados
|
||||||
|
**Proyecto:** Integración de beneficios
|
||||||
|
**Timeline:** 6 meses
|
||||||
|
**Mi Rol:** Arquitecto de Integración
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafío
|
||||||
|
|
||||||
|
El cliente tenía sistema interno de gestión de beneficios que necesitaba sincronizar con SAP ECC para procesar nómina.
|
||||||
|
|
||||||
|
### Dolores principales:
|
||||||
|
- Proceso manual sujeto a errores
|
||||||
|
- 3-5 días de delay entre sistemas
|
||||||
|
- 100k empleados esperando procesamiento
|
||||||
|
- Picos de carga (fin de mes)
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
- Presupuesto limitado (sin SAP BTP)
|
||||||
|
- Equipo SAP interno pequeño (2 desarrolladores)
|
||||||
|
- Plazo ajustado (6 meses go-live)
|
||||||
|
- Sistema legacy .NET Framework 4.5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solución
|
||||||
|
|
||||||
|
Arquitectura de integración bidireccional:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Sistema Interno] ←→ [Queue] ←→ [SAP Connector] ←→ [SAP ECC]
|
||||||
|
↓ ↓
|
||||||
|
[MongoDB Logs] [ABAP Z_BENEFITS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Componentes:
|
||||||
|
- .NET Service con SAP Connector (NCo 3.0)
|
||||||
|
- ABAP transaction personalizada (Z_BENEFITS)
|
||||||
|
- Queue system (RabbitMQ) para retry logic
|
||||||
|
- MongoDB para auditoría y troubleshooting
|
||||||
|
- Scheduler (Hangfire) para batch processing
|
||||||
|
|
||||||
|
### Flujo:
|
||||||
|
1. Sistema genera cambios (new hire, alteraciones)
|
||||||
|
2. Service procesa batch (500 registros/vez)
|
||||||
|
3. SAP Connector llama Z_BENEFITS vía RFC
|
||||||
|
4. SAP retorna estado (éxito/error)
|
||||||
|
5. Retry automático si falla (máx 3x)
|
||||||
|
6. Logs MongoDB para troubleshooting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultado
|
||||||
|
|
||||||
|
### Métricas:
|
||||||
|
- **100k+** transacciones/día procesadas
|
||||||
|
- **99.9%** uptime
|
||||||
|
- Reducción de **5 días → 4 horas** (delay)
|
||||||
|
- **80%** reducción tiempo procesamiento
|
||||||
|
- **Zero** errores manuales (vs 2-3% antes)
|
||||||
|
|
||||||
|
### Beneficios:
|
||||||
|
- Empleados reciben beneficios a tiempo
|
||||||
|
- Equipo RRHH economiza 40h/mes (trabajo manual)
|
||||||
|
- Auditoría completa (compliance)
|
||||||
|
- Escalable (crecimiento 30% año sin refactor)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`C#` `.NET Framework 4.5` `SAP NCo 3.0` `RabbitMQ` `MongoDB` `Hangfire` `Docker` `SAP ECC` `ABAP` `RFC`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisiones & Motivación
|
||||||
|
|
||||||
|
### 💡 Decisión 1: SAP Connector vs SAP BTP
|
||||||
|
|
||||||
|
**Opciones evaluadas:**
|
||||||
|
- SAP BTP (eventos, APIs modernas, cloud)
|
||||||
|
- SAP Connector (RFC directo, on-premise)
|
||||||
|
|
||||||
|
**Elegimos:** SAP Connector
|
||||||
|
|
||||||
|
**Motivación:**
|
||||||
|
- Cliente tenía SAP ECC on-premise (no S/4)
|
||||||
|
- Presupuesto no permitía licencia BTP
|
||||||
|
- Equipo SAP cómodo con ABAP/RFC
|
||||||
|
- Necesidades atendidas con RFC (no necesitaba event-driven real-time)
|
||||||
|
|
||||||
|
**Trade-off aceptado:**
|
||||||
|
- Menos "moderno" que BTP, pero 100% funcional
|
||||||
|
- Costo $0 adicional vs $30k+/año BTP
|
||||||
|
- Delivery 2 meses más rápido (sin learning curve BTP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💡 Decisión 2: Queue System vs Llamadas Directas
|
||||||
|
|
||||||
|
**Opciones evaluadas:**
|
||||||
|
- Llamadas síncronas directas (más simple)
|
||||||
|
- Queue con retry (más complejo)
|
||||||
|
|
||||||
|
**Elegimos:** Queue + Retry
|
||||||
|
|
||||||
|
**Motivación:**
|
||||||
|
- SAP ocasionalmente indisponible (mantenimiento)
|
||||||
|
- Picos de carga (fin de mes = 200k reqs)
|
||||||
|
- Garantizar zero pérdida de datos
|
||||||
|
- Resiliencia > simplicidad (ambiente crítico)
|
||||||
|
|
||||||
|
**Implementación:**
|
||||||
|
- RabbitMQ con dead-letter queue
|
||||||
|
- Retry exponencial (1min, 5min, 15min)
|
||||||
|
- Alertas si 3 fallas consecutivas
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
- Zero pérdida datos en 2 años producción
|
||||||
|
- Equipo RRHH no necesita "estar pendiente"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💡 Decisión 3: ABAP Personalizado vs Standard
|
||||||
|
|
||||||
|
**Opciones evaluadas:**
|
||||||
|
- BAPIs standard SAP (zero código ABAP)
|
||||||
|
- Transaction personalizada (Z_BENEFITS)
|
||||||
|
|
||||||
|
**Elegimos:** Transaction personalizada
|
||||||
|
|
||||||
|
**Motivación:**
|
||||||
|
- BAPIs standard no tenían validaciones específicas del negocio
|
||||||
|
- Cliente quería lógica centralizada en SAP (single source of truth)
|
||||||
|
- Permitió validaciones complejas (elegibilidad, dependientes, límites)
|
||||||
|
|
||||||
|
**Trade-off:**
|
||||||
|
- Requiere mantenimiento ABAP (equipo SAP interno)
|
||||||
|
- Pero: Cliente prefirió vs lógica dual (riesgo desincronización)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ Alternativas NO Elegidas
|
||||||
|
|
||||||
|
**Webhook/Callback (Event-Driven):**
|
||||||
|
- Cliente no tenía infraestructura exponer APIs
|
||||||
|
- Sistema interno detrás de firewall
|
||||||
|
- Polling batch funciona bien para el caso
|
||||||
|
|
||||||
|
**Microservicios Kubernetes:**
|
||||||
|
- Overkill para integración única
|
||||||
|
- Equipo no tenía expertise K8s
|
||||||
|
- Docker simple suficiente
|
||||||
|
|
||||||
|
**Real-time Sync (<1min):**
|
||||||
|
- Negocio no necesita (batch diario ok)
|
||||||
|
- Costo infra aumentaría 3x
|
||||||
|
- 4h delay es aceptable para nómina
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aprendizajes
|
||||||
|
|
||||||
|
### ✅ Lo que funcionó muy bien:
|
||||||
|
- Involucrar equipo SAP desde día 1 (buy-in)
|
||||||
|
- MongoDB para logs (troubleshooting 10x más rápido)
|
||||||
|
- Retry logic salvó innumerables veces
|
||||||
|
|
||||||
|
### 🔄 Lo que haría diferente:
|
||||||
|
- Agregar health check endpoint más temprano
|
||||||
|
- Dashboard de monitoreo desde inicio (agregamos después)
|
||||||
|
|
||||||
|
### 📚 Lecciones para próximos proyectos:
|
||||||
|
- Cliente "presupuesto limitado" ≠ "solución limitada" - creatividad resuelve
|
||||||
|
- Documentar TODAS decisiones arquitecturales (team turnover)
|
||||||
|
- Simplicidad vence complejidad cuando ambas funcionan (KISS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ¿Necesita Algo Similar?
|
||||||
|
|
||||||
|
Integraciones SAP complejas, sistemas legacy, o arquitectura de alta disponibilidad?
|
||||||
|
|
||||||
|
[Conversemos sobre su desafío →](/#contact)
|
||||||
329
Content/Cases/pt/asp-to-dotnet-migration.md
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
---
|
||||||
|
title: "Migração ASP 3.0 para .NET Core - Sistema de Rastreamento de Cargas"
|
||||||
|
slug: "asp-to-dotnet-migration"
|
||||||
|
summary: "Tech Lead na migração gradual de sistema crítico ASP 3.0 para .NET Core, com sincronização de dados entre versões e redução de custos de R$ 100k/ano em APIs de mapeamento."
|
||||||
|
client: "Empresa de Logística e Rastreamento"
|
||||||
|
industry: "Logística & Segurança"
|
||||||
|
timeline: "12 meses (migração completa)"
|
||||||
|
role: "Tech Lead & Solution Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- ASP Classic
|
||||||
|
- .NET Core
|
||||||
|
- SQL Server
|
||||||
|
- Migration
|
||||||
|
- Tech Lead
|
||||||
|
- OSRM
|
||||||
|
- APIs
|
||||||
|
- Arquitetura
|
||||||
|
featured: true
|
||||||
|
order: 2
|
||||||
|
date: 2015-06-01
|
||||||
|
seo_title: "Migração ASP 3.0 para .NET Core - Case Carneiro Tech"
|
||||||
|
seo_description: "Case de migração gradual de aplicação ASP 3.0 para .NET Core com sincronização de dados e redução de R$ 100k/ano em custos de APIs."
|
||||||
|
seo_keywords: "ASP migration, .NET Core, legacy modernization, SQL Server, OSRM, tech lead, routing API"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Sistema crítico de monitoramento de cargas de alto valor (TVs LED de R$ 3.000 cada, carregamentos de até 1000 unidades) usando rastreamento GPS via satélite. A aplicação cobria todo o ciclo: desde cadastro e avaliação de motoristas (background check policial) até monitoramento em tempo real e entrega final.
|
||||||
|
|
||||||
|
**Desafio principal:** Migrar aplicação ASP 3.0 legada para .NET Core sem downtime, mantendo operação crítica 24/7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Sistema Legado Crítico
|
||||||
|
|
||||||
|
A empresa operava um sistema mission-critical em **ASP 3.0** (Classic ASP) que não podia parar:
|
||||||
|
|
||||||
|
**Tecnologia legada:**
|
||||||
|
- ASP 3.0 (tecnologia de 1998)
|
||||||
|
- SQL Server 2005
|
||||||
|
- Cluster failover on-premises (perfeitamente capaz de suportar a carga)
|
||||||
|
- Integração com rastreadores GPS via satélite
|
||||||
|
- Google Maps API (custo: **R$ 100.000/ano** apenas para cálculo de rotas)
|
||||||
|
|
||||||
|
**Restrições:**
|
||||||
|
- Sistema operando 24/7 com cargas de alto valor
|
||||||
|
- Impossibilidade de downtime durante migração
|
||||||
|
- Múltiplos módulos interdependentes
|
||||||
|
- Equipe precisava continuar desenvolvendo features durante a migração
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### Fase 1: Preparação da Infraestrutura (Meses 1-3)
|
||||||
|
|
||||||
|
#### Upgrade do Banco de Dados
|
||||||
|
```
|
||||||
|
SQL Server 2005 → SQL Server 2014
|
||||||
|
- Backup completo e validação
|
||||||
|
- Migração de stored procedures
|
||||||
|
- Otimização de índices
|
||||||
|
- Testes de performance
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Estratégia de Sincronização Dual-Write
|
||||||
|
|
||||||
|
Implementei um **sistema de sincronização bidirecional** que permitia:
|
||||||
|
|
||||||
|
1. **Módulos novos (.NET Core)** gravavam no banco novo
|
||||||
|
2. **Trigger automático** sincronizava dados para o banco legado
|
||||||
|
3. **Módulos antigos (ASP 3.0)** continuavam funcionando normalmente
|
||||||
|
4. **Zero downtime** durante toda a migração
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Exemplo de sincronização implementada
|
||||||
|
public class DualWriteService
|
||||||
|
{
|
||||||
|
public async Task SaveDriver(Driver driver)
|
||||||
|
{
|
||||||
|
// Grava no banco novo (.NET Core)
|
||||||
|
await _newDbContext.Drivers.AddAsync(driver);
|
||||||
|
await _newDbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Trigger SQL sincroniza automaticamente para banco legado
|
||||||
|
// Módulos ASP 3.0 continuam funcionando
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Por que essa abordagem?**
|
||||||
|
- Permitiu migração **módulo por módulo**
|
||||||
|
- Equipe podia continuar desenvolvendo
|
||||||
|
- Rollback simples se necessário
|
||||||
|
- Redução de risco operacional
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Migração Gradual dos Módulos (Meses 4-12)
|
||||||
|
|
||||||
|
Migrei os módulos em ordem de complexidade crescente:
|
||||||
|
|
||||||
|
**Ordem de migração:**
|
||||||
|
1. ✅ Cadastros básicos (motoristas, veículos)
|
||||||
|
2. ✅ Avaliação de risco (integração com base policial)
|
||||||
|
3. ✅ Gestão de cargas e rotas
|
||||||
|
4. ✅ Monitoramento GPS em tempo real
|
||||||
|
5. ✅ Alertas e notificações
|
||||||
|
6. ✅ Relatórios e analytics
|
||||||
|
|
||||||
|
**Stack da aplicação migrada:**
|
||||||
|
- `.NET Core 1.0` (2015-2016 era o início do .NET Core)
|
||||||
|
- `Entity Framework Core`
|
||||||
|
- `SignalR` para monitoramento real-time
|
||||||
|
- `SQL Server 2014`
|
||||||
|
- APIs RESTful
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Redução de Custos com OSRM (Economia de R$ 100k/ano)
|
||||||
|
|
||||||
|
#### Problema: Custo Proibitivo do Google Maps
|
||||||
|
|
||||||
|
A empresa gastava **R$ 100.000/ano** apenas com Google Maps Directions API para cálculo de rotas dos caminhões.
|
||||||
|
|
||||||
|
#### Solução: OSRM (Open Source Routing Machine)
|
||||||
|
|
||||||
|
Implementei uma solução baseada em **OSRM** (motor de roteamento open-source):
|
||||||
|
|
||||||
|
**Arquitetura da solução:**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Frontend │
|
||||||
|
│ (Leaflet.js) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ┌──────────────┐
|
||||||
|
│ API Wrapper │─────▶│ OSRM Server │
|
||||||
|
│ (.NET Core) │ │ (self-hosted)│
|
||||||
|
└────────┬────────┘ └──────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Google Maps │
|
||||||
|
│ (display only) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Implementação:**
|
||||||
|
|
||||||
|
1. **OSRM Server configurado** em servidor próprio
|
||||||
|
2. **API wrapper amigável** em .NET Core que:
|
||||||
|
- Recebia origem/destino
|
||||||
|
- Consultava OSRM (gratuito)
|
||||||
|
- Retornava todos os pontos da rota
|
||||||
|
- Formatava para o frontend
|
||||||
|
3. **Frontend** desenhava a rota no Google Maps (apenas visualização, sem API de rotas)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[HttpGet("route")]
|
||||||
|
public async Task<IActionResult> GetRoute(double originLat, double originLng,
|
||||||
|
double destLat, double destLng)
|
||||||
|
{
|
||||||
|
// Consulta OSRM (gratuito)
|
||||||
|
var osrmResponse = await _osrmClient.GetRouteAsync(
|
||||||
|
originLat, originLng, destLat, destLng);
|
||||||
|
|
||||||
|
// Retorna pontos formatados para o frontend
|
||||||
|
return Ok(new {
|
||||||
|
points = osrmResponse.Routes[0].Geometry.Coordinates,
|
||||||
|
distance = osrmResponse.Routes[0].Distance,
|
||||||
|
duration = osrmResponse.Routes[0].Duration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontend com Leaflet:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Desenha rota no mapa (Google Maps apenas para tiles)
|
||||||
|
L.polyline(routePoints, {color: 'red'}).addTo(map);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tentativa com OpenStreetMap
|
||||||
|
|
||||||
|
Tentei substituir também o Google Maps (tiles) por **OpenStreetMap**, que funcionou tecnicamente, mas:
|
||||||
|
|
||||||
|
❌ **Usuários não gostaram** da aparência
|
||||||
|
❌ Preferiam a interface familiar do Google Maps
|
||||||
|
|
||||||
|
✅ **Decisão:** Manter Google Maps apenas para visualização (sem custo de API de rotas)
|
||||||
|
|
||||||
|
**Resultado:** Economia de **~R$ 100.000/ano** mantendo qualidade das rotas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Migração Completa em 12 Meses
|
||||||
|
|
||||||
|
✅ **100% dos módulos** migrados de ASP 3.0 para .NET Core
|
||||||
|
✅ **Zero downtime** durante toda a migração
|
||||||
|
✅ **Equipe produtiva** durante todo o processo
|
||||||
|
✅ Sistema mais rápido e escalável
|
||||||
|
|
||||||
|
### Redução de Custos
|
||||||
|
|
||||||
|
💰 **R$ 100.000/ano economizados** com substituição do Google Maps Directions API
|
||||||
|
📉 **Infraestrutura otimizada** com SQL Server 2014
|
||||||
|
|
||||||
|
### Melhorias Técnicas
|
||||||
|
|
||||||
|
🚀 **Performance:** Aplicação .NET Core 3x mais rápida que ASP 3.0
|
||||||
|
🔒 **Segurança:** Stack moderna com patches de segurança ativos
|
||||||
|
🛠️ **Manutenibilidade:** Código C# moderno vs VBScript legado
|
||||||
|
📊 **Monitoramento:** SignalR para tracking real-time mais eficiente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fase Não Executada: Microserviços & Cloud
|
||||||
|
|
||||||
|
### Planejamento Inicial
|
||||||
|
|
||||||
|
Participei do **design e concepção** da segunda fase (nunca executada):
|
||||||
|
|
||||||
|
**Arquitetura planejada:**
|
||||||
|
- Migração para **Azure** (cloud estava apenas começando em 2015)
|
||||||
|
- Quebra em **microserviços**:
|
||||||
|
- Serviço de autenticação
|
||||||
|
- Serviço de GPS/tracking
|
||||||
|
- Serviço de rotas
|
||||||
|
- Serviço de notificações
|
||||||
|
- **Event-driven architecture** com message queues
|
||||||
|
|
||||||
|
**Por que não foi executada:**
|
||||||
|
|
||||||
|
Saí da empresa logo após concluir a migração para .NET Core. A segunda fase ficou planejada mas não foi implementada por mim.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`ASP 3.0` `VBScript` `.NET Core 1.0` `C#` `Entity Framework Core` `SQL Server 2005` `SQL Server 2014` `OSRM` `Leaflet.js` `Google Maps` `SignalR` `REST APIs` `GPS/Satellite` `Migration Strategy` `Dual-Write Pattern`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Por que sincronização dual-write?
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
1. ❌ Big Bang migration (muito arriscado)
|
||||||
|
2. ❌ Manter tudo em ASP 3.0 (insustentável)
|
||||||
|
3. ✅ **Migração gradual com sync** (escolhido)
|
||||||
|
|
||||||
|
**Justificativa:**
|
||||||
|
- Sistema crítico não podia parar
|
||||||
|
- Permitiu rollback módulo por módulo
|
||||||
|
- Equipe continuou produtiva
|
||||||
|
|
||||||
|
### Por que OSRM em vez de outros?
|
||||||
|
|
||||||
|
**Alternativas:**
|
||||||
|
- Google Maps: R$ 100k/ano ❌
|
||||||
|
- Mapbox: Licença paga ❌
|
||||||
|
- GraphHopper: Configuração complexa ❌
|
||||||
|
- **OSRM: Open-source, rápido, configurável** ✅
|
||||||
|
|
||||||
|
### Por que não OpenStreetMap para tiles?
|
||||||
|
|
||||||
|
**Decisão baseada em UX:**
|
||||||
|
- Tecnicamente funcionou perfeitamente
|
||||||
|
- Usuários preferiam interface familiar do Google
|
||||||
|
- **Compromisso:** Google Maps para visualização (grátis) + OSRM para rotas (grátis)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Migração Gradual > Big Bang
|
||||||
|
|
||||||
|
Migrar módulo por módulo com sincronização permitiu:
|
||||||
|
- Aprendizado contínuo
|
||||||
|
- Ajustes de rota durante o processo
|
||||||
|
- Confiança da equipe e stakeholders
|
||||||
|
|
||||||
|
### 2. Open Source Pode Economizar Muito
|
||||||
|
|
||||||
|
OSRM economizou **R$ 100k/ano** sem perda de qualidade. Mas requer:
|
||||||
|
- Expertise para configurar
|
||||||
|
- Infraestrutura própria
|
||||||
|
- Manutenção contínua
|
||||||
|
|
||||||
|
### 3. UX > Tecnologia às Vezes
|
||||||
|
|
||||||
|
OpenStreetMap era tecnicamente superior (gratuito), mas usuários preferiram Google Maps. **Lição:** Ouvir os usuários finais.
|
||||||
|
|
||||||
|
### 4. Planeje Cloud, mas Valide o ROI
|
||||||
|
|
||||||
|
Em 2015, cloud estava começando. A infraestrutura on-premises (cluster SQL Server) era perfeitamente capaz. **Não force cloud se não há benefício claro.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context: Por que 2015 foi um Momento Especial?
|
||||||
|
|
||||||
|
**Estado da tecnologia em 2015:**
|
||||||
|
|
||||||
|
- ☁️ **Cloud engatinhando:** AWS existia, Azure crescendo, mas adoção corporativa ainda baixa
|
||||||
|
- 🆕 **.NET Core 1.0 lançado** em junho/2016 (usamos RC durante projeto)
|
||||||
|
- 📱 **Microserviços:** Conceito novo, Docker em adoção inicial
|
||||||
|
- 🗺️ **Google Maps dominante:** APIs pagas, poucas alternativas open-source maduras
|
||||||
|
|
||||||
|
**Desafios da época:**
|
||||||
|
- Ferramentas de migração ASP→.NET inexistentes
|
||||||
|
- Documentação .NET Core escassa (versão 1.0!)
|
||||||
|
- Padrões de arquitetura ainda se consolidando
|
||||||
|
|
||||||
|
Este projeto foi **pioneiro** ao adotar .NET Core logo no início, quando a maioria migrava para .NET Framework 4.x.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** Migração bem-sucedida de sistema crítico 24/7, economia de R$ 100k/ano, e base sólida para evolução futura.
|
||||||
|
|
||||||
|
[Quer discutir uma migração similar? Entre em contato](#contact)
|
||||||
382
Content/Cases/pt/cnpj-fast-process.md
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
---
|
||||||
|
title: "CNPJ Fast - Processo de Migração para CNPJ Alfanumérico"
|
||||||
|
slug: "cnpj-fast-process"
|
||||||
|
summary: "Criação de metodologia estruturada para migração de aplicações ao novo formato de CNPJ alfanumérico brasileiro, vendida para seguradora e empresa de cobrança."
|
||||||
|
client: "Empresa de Consultoria (Interno)"
|
||||||
|
industry: "Consultoria & Transformação Digital"
|
||||||
|
timeline: "3 meses (criação do processo)"
|
||||||
|
role: "Solution Architect & Process Designer"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- Process Design
|
||||||
|
- CNPJ
|
||||||
|
- Migration Strategy
|
||||||
|
- Regulatory Compliance
|
||||||
|
- Consulting
|
||||||
|
- Sales Enablement
|
||||||
|
featured: true
|
||||||
|
order: 3
|
||||||
|
date: 2024-09-01
|
||||||
|
seo_title: "CNPJ Fast - Metodologia de Migração CNPJ Alfanumérico"
|
||||||
|
seo_description: "Case de criação de processo estruturado para migração ao CNPJ alfanumérico brasileiro, vendido para seguradora e empresa de cobrança."
|
||||||
|
seo_keywords: "CNPJ alfanumérico, migration process, regulatory compliance, consulting, methodology"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Com a introdução do **CNPJ alfanumérico** pela Receita Federal brasileira, empresas enfrentavam o desafio de adaptar suas aplicações legadas que armazenavam CNPJ como campos numéricos (`bigint`, `numeric`, `int`).
|
||||||
|
|
||||||
|
Criei o **CNPJ Fast**, uma metodologia estruturada para avaliar, planejar e executar migrações de CNPJ em aplicações e bancos de dados corporativos.
|
||||||
|
|
||||||
|
**Resultado:** Processo vendido para **2 clientes** (seguradora e empresa de cobrança) antes mesmo da implementação.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Mudança Regulatória Complexa
|
||||||
|
|
||||||
|
**Contexto regulatório:**
|
||||||
|
- Receita Federal brasileira introduziu **CNPJ alfanumérico**
|
||||||
|
- CNPJ deixa de ser apenas números (14 dígitos)
|
||||||
|
- Passa a aceitar **letras e números** (formato alfanumérico)
|
||||||
|
|
||||||
|
**Impacto nas empresas:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ANTES: CNPJ numérico
|
||||||
|
CNPJ BIGINT -- 12345678000190
|
||||||
|
|
||||||
|
-- DEPOIS: CNPJ alfanumérico
|
||||||
|
CNPJ VARCHAR(18) -- 12.ABC.678/0001-90
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problemas identificados:**
|
||||||
|
|
||||||
|
1. 🗄️ **Banco de dados:** Colunas `BIGINT`, `NUMERIC`, `INT` não suportam caracteres
|
||||||
|
2. 🔑 **Chaves primárias:** CNPJ usado como PK em várias tabelas
|
||||||
|
3. 🔗 **Foreign keys:** Relacionamentos entre tabelas
|
||||||
|
4. 📊 **Volume:** Milhões de registros para migrar
|
||||||
|
5. 💻 **Aplicações:** Validações, máscaras, regras de negócio
|
||||||
|
6. 🧪 **Testes:** Garantir integridade após migração
|
||||||
|
7. ⏱️ **Downtime:** Janelas de manutenção limitadas
|
||||||
|
|
||||||
|
**Sem um processo estruturado**, empresas arriscavam:
|
||||||
|
- Perda de dados
|
||||||
|
- Inconsistências no banco
|
||||||
|
- Aplicações quebradas
|
||||||
|
- Downtime prolongado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution: CNPJ Fast Process
|
||||||
|
|
||||||
|
### Metodologia em 5 Fases
|
||||||
|
|
||||||
|
Desenhei um processo estruturado que poderia ser replicado em diferentes clientes:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 1: DISCOVERY & ASSESSMENT │
|
||||||
|
│ - Inventário de aplicações │
|
||||||
|
│ - Análise de schemas de banco │
|
||||||
|
│ - Identificação de tabelas impactadas │
|
||||||
|
│ - Estimativa de volume de dados │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 2: IMPACT ANALYSIS │
|
||||||
|
│ - Mapeamento de dependências │
|
||||||
|
│ - Análise de chaves primárias/estrangeiras │
|
||||||
|
│ - Identificação de regras de negócio │
|
||||||
|
│ - Avaliação de risco │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 3: MIGRATION PLANNING │
|
||||||
|
│ - Estratégia de migração (phased commits) │
|
||||||
|
│ - Scripts SQL automatizados │
|
||||||
|
│ - Plano de rollback │
|
||||||
|
│ - Janelas de manutenção │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 4: EXECUTION │
|
||||||
|
│ - Migração de dados em lotes │
|
||||||
|
│ - Atualização de aplicações │
|
||||||
|
│ - Testes de integração │
|
||||||
|
│ - Validação de integridade │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FASE 5: VALIDATION & GO-LIVE │
|
||||||
|
│ - Testes de regressão │
|
||||||
|
│ - Validação de performance │
|
||||||
|
│ - Go-live coordenado │
|
||||||
|
│ - Monitoramento pós-migração │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 1: Discovery & Assessment
|
||||||
|
|
||||||
|
**Objetivo:** Entender o escopo completo da migração
|
||||||
|
|
||||||
|
**Entregáveis:**
|
||||||
|
|
||||||
|
1. **Inventário de Aplicações**
|
||||||
|
- Lista de aplicações que usam CNPJ
|
||||||
|
- Tecnologias (ASP 3.0, VB6, .NET, microserviços)
|
||||||
|
- Criticidade de cada aplicação
|
||||||
|
|
||||||
|
2. **Análise de Schema**
|
||||||
|
```sql
|
||||||
|
-- Script de descoberta automática
|
||||||
|
SELECT
|
||||||
|
t.TABLE_SCHEMA,
|
||||||
|
t.TABLE_NAME,
|
||||||
|
c.COLUMN_NAME,
|
||||||
|
c.DATA_TYPE,
|
||||||
|
c.CHARACTER_MAXIMUM_LENGTH
|
||||||
|
FROM INFORMATION_SCHEMA.TABLES t
|
||||||
|
JOIN INFORMATION_SCHEMA.COLUMNS c
|
||||||
|
ON t.TABLE_NAME = c.TABLE_NAME
|
||||||
|
WHERE c.COLUMN_NAME LIKE '%CNPJ%'
|
||||||
|
AND c.DATA_TYPE IN ('bigint', 'numeric', 'int')
|
||||||
|
ORDER BY t.TABLE_SCHEMA, t.TABLE_NAME;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Estimativa de Volume**
|
||||||
|
- Total de registros por tabela
|
||||||
|
- Tamanho em GB
|
||||||
|
- Tempo estimado de migração
|
||||||
|
|
||||||
|
**Exemplo de output:**
|
||||||
|
|
||||||
|
| Tabela | Coluna | Tipo Atual | Registros | Criticidade |
|
||||||
|
|--------|--------|------------|-----------|-------------|
|
||||||
|
| Clientes | CNPJ_Cliente | BIGINT | 8.000.000 | Alta |
|
||||||
|
| Fornecedores | CNPJ_Fornecedor | NUMERIC(14) | 2.500.000 | Média |
|
||||||
|
| Transações | CNPJ_Pagador | BIGINT | 90.000.000 | Crítica |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Impact Analysis
|
||||||
|
|
||||||
|
**Objetivo:** Mapear todas as dependências e riscos
|
||||||
|
|
||||||
|
**Análise de chaves:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Identifica PKs e FKs envolvendo CNPJ
|
||||||
|
SELECT
|
||||||
|
fk.name AS FK_Name,
|
||||||
|
tp.name AS Parent_Table,
|
||||||
|
cp.name AS Parent_Column,
|
||||||
|
tr.name AS Referenced_Table,
|
||||||
|
cr.name AS Referenced_Column
|
||||||
|
FROM sys.foreign_keys fk
|
||||||
|
INNER JOIN sys.tables tp ON fk.parent_object_id = tp.object_id
|
||||||
|
INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
|
||||||
|
INNER JOIN sys.columns cp ON fkc.parent_column_id = cp.column_id
|
||||||
|
AND fkc.parent_object_id = cp.object_id
|
||||||
|
INNER JOIN sys.tables tr ON fk.referenced_object_id = tr.object_id
|
||||||
|
INNER JOIN sys.columns cr ON fkc.referenced_column_id = cr.column_id
|
||||||
|
AND fkc.referenced_object_id = cr.object_id
|
||||||
|
WHERE cp.name LIKE '%CNPJ%' OR cr.name LIKE '%CNPJ%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Avaliação de Risco:**
|
||||||
|
|
||||||
|
- 🔴 **Alto:** Tabelas com CNPJ como PK e >10M registros
|
||||||
|
- 🟡 **Médio:** Tabelas com FK para CNPJ
|
||||||
|
- 🟢 **Baixo:** Tabelas sem constraints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Migration Planning
|
||||||
|
|
||||||
|
**Estratégia de migração gradual:**
|
||||||
|
|
||||||
|
Para evitar travamento de banco, desenhei estratégia de **phased commits**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Estratégia para tabelas grandes (>1M registros)
|
||||||
|
|
||||||
|
-- 1. Adicionar nova coluna VARCHAR
|
||||||
|
ALTER TABLE Clientes
|
||||||
|
ADD CNPJ_Cliente_New VARCHAR(18) NULL;
|
||||||
|
|
||||||
|
-- 2. Migração em lotes (commits faseados)
|
||||||
|
DECLARE @BatchSize INT = 100000;
|
||||||
|
DECLARE @RowsAffected INT = 1;
|
||||||
|
|
||||||
|
WHILE @RowsAffected > 0
|
||||||
|
BEGIN
|
||||||
|
UPDATE TOP (@BatchSize) Clientes
|
||||||
|
SET CNPJ_Cliente_New = FORMAT(CNPJ_Cliente, '00000000000000')
|
||||||
|
WHERE CNPJ_Cliente_New IS NULL;
|
||||||
|
|
||||||
|
SET @RowsAffected = @@ROWCOUNT;
|
||||||
|
WAITFOR DELAY '00:00:01'; -- Pausa entre lotes
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 3. Remover constraints (PKs, FKs)
|
||||||
|
ALTER TABLE Clientes DROP CONSTRAINT PK_Clientes;
|
||||||
|
|
||||||
|
-- 4. Renomear colunas
|
||||||
|
EXEC sp_rename 'Clientes.CNPJ_Cliente', 'CNPJ_Cliente_Old', 'COLUMN';
|
||||||
|
EXEC sp_rename 'Clientes.CNPJ_Cliente_New', 'CNPJ_Cliente', 'COLUMN';
|
||||||
|
|
||||||
|
-- 5. Recriar constraints
|
||||||
|
ALTER TABLE Clientes
|
||||||
|
ADD CONSTRAINT PK_Clientes PRIMARY KEY (CNPJ_Cliente);
|
||||||
|
|
||||||
|
-- 6. Remover coluna antiga (após validação)
|
||||||
|
ALTER TABLE Clientes DROP COLUMN CNPJ_Cliente_Old;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Por que essa abordagem?**
|
||||||
|
|
||||||
|
- ✅ Evita lock de tabela inteira
|
||||||
|
- ✅ Permite pausar/retomar migração
|
||||||
|
- ✅ Minimiza impacto em produção
|
||||||
|
- ✅ Facilita rollback se necessário
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4 & 5: Execution e Validation
|
||||||
|
|
||||||
|
**Checklist de execução:**
|
||||||
|
|
||||||
|
- [ ] Backup completo do banco
|
||||||
|
- [ ] Executar scripts de migração em lotes
|
||||||
|
- [ ] Atualizar aplicações (validações, máscaras)
|
||||||
|
- [ ] Testes de integração
|
||||||
|
- [ ] Validação de integridade referencial
|
||||||
|
- [ ] Testes de performance
|
||||||
|
- [ ] Go-live coordenado
|
||||||
|
- [ ] Monitoramento 24h pós-migração
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sales Enablement: Apresentação UX
|
||||||
|
|
||||||
|
**Colaboração com Gestor de UX:**
|
||||||
|
|
||||||
|
O gestor de UX da empresa criou uma **apresentação visual impactante** do processo CNPJ Fast:
|
||||||
|
|
||||||
|
**Conteúdo da apresentação:**
|
||||||
|
- 📊 Infográficos do processo de 5 fases
|
||||||
|
- 📈 Exemplos de estimativas de tempo/custo
|
||||||
|
- 🎯 Casos de uso (seguradoras, bancos, fintechs)
|
||||||
|
- ✅ Checklist executivo
|
||||||
|
- 📋 Templates de documentação
|
||||||
|
|
||||||
|
**Resultado:** Apresentação usada pelo time comercial para prospecção.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Vendas Realizadas
|
||||||
|
|
||||||
|
**Cliente 1: Seguradora**
|
||||||
|
- Stack: ASP 3.0, VB6 components, .NET, microserviços
|
||||||
|
- Escopo: Migração completa de aplicações legadas
|
||||||
|
- Status: **Projeto vendido** (execução por outra equipe)
|
||||||
|
- Valor: [Confidencial]
|
||||||
|
|
||||||
|
**Cliente 2: Empresa de Cobrança**
|
||||||
|
- Escopo: Migração de banco de dados (~100M registros)
|
||||||
|
- Status: **Projeto vendido e em execução** (por mim)
|
||||||
|
- Particularidade: Processo foi **reestruturado** para atender necessidades específicas
|
||||||
|
- Ver case completo: [Migração CNPJ - 100M Registros](/cases/cnpj-migration-database)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto no Negócio
|
||||||
|
|
||||||
|
💰 **2 projetos vendidos** antes mesmo da primeira execução
|
||||||
|
📈 **Processo replicável** para novos clientes
|
||||||
|
🎯 **Posicionamento** como especialista em migrações regulatórias
|
||||||
|
📚 **Base de conhecimento** para futuros projetos similares
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto Técnico
|
||||||
|
|
||||||
|
🔧 **Metodologia testada** em cenários reais
|
||||||
|
📖 **Documentação reutilizável** (scripts, checklists, templates)
|
||||||
|
🚀 **Aceleração** de projetos similares (de semanas para dias)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`SQL Server` `Migration Strategy` `Process Design` `Regulatory Compliance` `ASP 3.0` `VB6` `.NET` `Microservices` `Batch Processing` `Database Optimization`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Por que processo estruturado?
|
||||||
|
|
||||||
|
**Alternativas:**
|
||||||
|
1. ❌ Abordagem ad-hoc por projeto
|
||||||
|
2. ❌ Consultoria manual sem metodologia
|
||||||
|
3. ✅ **Processo replicável e escalável**
|
||||||
|
|
||||||
|
**Justificativa:**
|
||||||
|
- Reduz tempo de Discovery
|
||||||
|
- Padroniza entregas
|
||||||
|
- Facilita vendas (apresentação pronta)
|
||||||
|
- Permite execução por diferentes equipes
|
||||||
|
|
||||||
|
### Por que separar em 5 fases?
|
||||||
|
|
||||||
|
**Benefícios:**
|
||||||
|
- Cliente pode aprovar fase a fase
|
||||||
|
- Permite ajustes durante o processo
|
||||||
|
- Facilita gestão de riscos
|
||||||
|
- Entregas incrementais
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. UX/Apresentação Importa para Vendas
|
||||||
|
|
||||||
|
A apresentação visual feita pelo gestor de UX foi **crucial** para fechar os 2 contratos. Processo técnico bom + apresentação ruim = sem vendas.
|
||||||
|
|
||||||
|
### 2. Processo Vende, Não Apenas Execução
|
||||||
|
|
||||||
|
Criar uma **metodologia documentada** tem mais valor comercial do que apenas oferecer "horas de consultoria".
|
||||||
|
|
||||||
|
### 3. Cada Cliente é Único
|
||||||
|
|
||||||
|
O cliente pediu **reestruturação do processo**. Um bom processo deve ser:
|
||||||
|
- Estruturado o suficiente para ser replicável
|
||||||
|
- Flexível o suficiente para customizar
|
||||||
|
|
||||||
|
### 4. Colaboração Multidisciplinar
|
||||||
|
|
||||||
|
Trabalhar com o gestor de UX (apresentações) + time comercial (vendas) + técnico (execução) = sucesso.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**Oportunidades futuras:**
|
||||||
|
|
||||||
|
1. 🌎 **Expansão:** Oferecer CNPJ Fast para mais setores (bancos, fintechs, varejo)
|
||||||
|
2. 📦 **Produto:** Transformar em ferramenta automatizada (SaaS)
|
||||||
|
3. 📚 **Treinamento:** Capacitar equipes internas de clientes
|
||||||
|
4. 🔄 **Evolução:** Adaptar processo para outras migrações regulatórias (PIX, Open Banking)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** Metodologia estruturada que virou produto vendável, gerando receita antes mesmo da primeira execução técnica.
|
||||||
|
|
||||||
|
[Quer implementar CNPJ Fast na sua empresa? Entre em contato](#contact)
|
||||||
469
Content/Cases/pt/cnpj-migration-database.md
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
---
|
||||||
|
title: "Migração CNPJ Alfanumérico - 100 Milhões de Registros"
|
||||||
|
slug: "cnpj-migration-database"
|
||||||
|
summary: "Execução de migração massiva de CNPJ numérico para alfanumérico em banco de dados com ~100M registros, usando estratégia de commits faseados para evitar travamento."
|
||||||
|
client: "Empresa de Cobrança"
|
||||||
|
industry: "Cobrança & Serviços Financeiros"
|
||||||
|
timeline: "Em execução"
|
||||||
|
role: "Database Architect & Tech Lead"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- SQL Server
|
||||||
|
- Database Migration
|
||||||
|
- CNPJ
|
||||||
|
- Performance Optimization
|
||||||
|
- Batch Processing
|
||||||
|
- Big Data
|
||||||
|
featured: true
|
||||||
|
order: 4
|
||||||
|
date: 2024-11-01
|
||||||
|
seo_title: "Migração CNPJ Alfanumérico - 100M Registros | Carneiro Tech"
|
||||||
|
seo_description: "Case de migração massiva de CNPJ em banco de dados com 100 milhões de registros usando commits faseados e otimizações de performance."
|
||||||
|
seo_keywords: "database migration, SQL Server, CNPJ, batch processing, performance optimization, phased commits"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Uma empresa de cobrança que trabalha com bancos de dados de informação transitória (sem software proprietário) precisa adaptar seus sistemas ao novo formato de **CNPJ alfanumérico** brasileiro.
|
||||||
|
|
||||||
|
**Desafio principal:** Migrar ~**100 milhões de registros** em tabelas com colunas `BIGINT` e `NUMERIC` para `VARCHAR`, sem travar o banco de dados em produção.
|
||||||
|
|
||||||
|
**Status:** Projeto em execução (preparação de scripts de migração).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Volume Massivo de Dados
|
||||||
|
|
||||||
|
**Contexto da empresa:**
|
||||||
|
- Empresa de cobrança (não desenvolve software próprio)
|
||||||
|
- Trabalha com **dados transitórios** (alta rotatividade)
|
||||||
|
- Banco de dados SQL Server com volume crítico
|
||||||
|
|
||||||
|
**Análise inicial revelou:**
|
||||||
|
|
||||||
|
| Tabela | Coluna | Tipo Atual | Registros | Tamanho |
|
||||||
|
|--------|--------|------------|-----------|---------|
|
||||||
|
| Devedores | CNPJ_Devedor | BIGINT | 8.000.000 | 60 GB |
|
||||||
|
| Transações | CNPJ_Pagador | NUMERIC(14) | 90.000.000 | 1.2 TB |
|
||||||
|
| Empresas | CNPJ_Empresa | BIGINT | 2.500.000 | 18 GB |
|
||||||
|
| **TOTAL** | - | - | **~100.000.000** | **~1.3 TB** |
|
||||||
|
|
||||||
|
**Problemas identificados:**
|
||||||
|
|
||||||
|
1. 🔴 **Tabelas com 8M+ linhas** usando `BIGINT` para CNPJ
|
||||||
|
2. 🔴 **90 milhões de registros** em tabela de transações
|
||||||
|
3. 🔑 **CNPJ como chave primária** em algumas tabelas
|
||||||
|
4. 🔗 **Foreign keys** relacionando múltiplas tabelas
|
||||||
|
5. ⚠️ **Impossibilidade de downtime prolongado** (operação 24/7)
|
||||||
|
6. 💾 **Restrições de espaço** em disco (precisa estratégia eficiente)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Strategic Decision: Phased Commits
|
||||||
|
|
||||||
|
### Por que NÃO fazer ALTER COLUMN direto?
|
||||||
|
|
||||||
|
**Abordagem ingênua (NÃO funciona):**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- ❌ NUNCA FAÇA ISSO EM TABELAS GRANDES
|
||||||
|
ALTER TABLE Transacoes
|
||||||
|
ALTER COLUMN CNPJ_Pagador VARCHAR(18);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problemas:**
|
||||||
|
- 🔒 Trava a tabela inteira durante a conversão
|
||||||
|
- ⏱️ Pode levar horas/dias em tabelas grandes
|
||||||
|
- 💥 Bloqueia todas as operações (INSERT, UPDATE, SELECT)
|
||||||
|
- 🚨 Risco de timeout ou falha no meio da operação
|
||||||
|
- 🔙 Rollback complexo se algo der errado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Estratégia Escolhida: Column Swap com Commits Faseados
|
||||||
|
|
||||||
|
**Baseado em experiência anterior**, decidi usar abordagem gradual:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 1. Criar nova coluna VARCHAR no FINAL │
|
||||||
|
│ (operação rápida, não bloqueia tabela) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 2. UPDATE em lotes (commits faseados) │
|
||||||
|
│ - 100k registros por vez │
|
||||||
|
│ - Pausa entre lotes (evita contenção) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 3. Remover PKs e FKs │
|
||||||
|
│ (após 100% migrado) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 4. Renomear colunas (swap) │
|
||||||
|
│ - CNPJ → CNPJ_Old │
|
||||||
|
│ - CNPJ_New → CNPJ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 5. Recriar PKs/FKs com nova coluna │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ 6. Validação e exclusão da coluna antiga │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Por que essa abordagem?**
|
||||||
|
|
||||||
|
✅ **Sem lock de tabela completa** (operação incremental)
|
||||||
|
✅ **Pode pausar/retomar** a qualquer momento
|
||||||
|
✅ **Monitoramento de progresso** em tempo real
|
||||||
|
✅ **Rollback simples** (basta dropar nova coluna)
|
||||||
|
✅ **Minimiza impacto em produção** (commits pequenos)
|
||||||
|
|
||||||
|
**Decisão tomada baseada em:**
|
||||||
|
- 📚 Experiência anterior com migrações de grande volume
|
||||||
|
- 🔍 Conhecimento de locks do SQL Server
|
||||||
|
- 🎯 Necessidade de zero downtime
|
||||||
|
|
||||||
|
**Nota:** Essa decisão foi tomada **sem consultar IA** - baseada puramente em experiência prática de projetos anteriores.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Fase 1: Criar Nova Coluna
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Operação rápida (metadata change apenas)
|
||||||
|
ALTER TABLE Transacoes
|
||||||
|
ADD CNPJ_Pagador_New VARCHAR(18) NULL;
|
||||||
|
|
||||||
|
-- Adiciona índice temporário para acelerar lookups
|
||||||
|
CREATE NONCLUSTERED INDEX IX_Temp_CNPJ_New
|
||||||
|
ON Transacoes(CNPJ_Pagador_New)
|
||||||
|
WHERE CNPJ_Pagador_New IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tempo estimado:** ~1 segundo (independente do tamanho da tabela)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Migração em Lotes (Core Strategy)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Script de migração com commits faseados
|
||||||
|
DECLARE @BatchSize INT = 100000; -- 100k registros por lote
|
||||||
|
DECLARE @RowsAffected INT = 1;
|
||||||
|
DECLARE @TotalProcessed INT = 0;
|
||||||
|
DECLARE @StartTime DATETIME = GETDATE();
|
||||||
|
|
||||||
|
WHILE @RowsAffected > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Atualiza lote de 100k registros ainda não migrados
|
||||||
|
UPDATE TOP (@BatchSize) Transacoes
|
||||||
|
SET CNPJ_Pagador_New = RIGHT('00000000000000' + CAST(CNPJ_Pagador AS VARCHAR), 14)
|
||||||
|
WHERE CNPJ_Pagador_New IS NULL;
|
||||||
|
|
||||||
|
SET @RowsAffected = @@ROWCOUNT;
|
||||||
|
SET @TotalProcessed = @TotalProcessed + @RowsAffected;
|
||||||
|
|
||||||
|
COMMIT TRANSACTION;
|
||||||
|
|
||||||
|
-- Log de progresso
|
||||||
|
PRINT 'Processed: ' + CAST(@TotalProcessed AS VARCHAR) + ' rows. Batch: ' + CAST(@RowsAffected AS VARCHAR);
|
||||||
|
PRINT 'Elapsed time: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR) + ' seconds';
|
||||||
|
|
||||||
|
-- Pausa entre lotes (reduz contenção)
|
||||||
|
WAITFOR DELAY '00:00:01'; -- 1 segundo entre lotes
|
||||||
|
END;
|
||||||
|
|
||||||
|
PRINT 'Migration completed! Total rows: ' + CAST(@TotalProcessed AS VARCHAR);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros configuráveis:**
|
||||||
|
|
||||||
|
- `@BatchSize`: 100k (balanceado entre performance e lock time)
|
||||||
|
- Muito pequeno = muitas transações, overhead
|
||||||
|
- Muito grande = lock prolongado, impacto em prod
|
||||||
|
- `WAITFOR DELAY`: 1 segundo (dá tempo de outras queries rodarem)
|
||||||
|
|
||||||
|
**Estimativas de tempo:**
|
||||||
|
|
||||||
|
| Registros | Batch Size | Tempo Estimado |
|
||||||
|
|-----------|------------|----------------|
|
||||||
|
| 8.000.000 | 100.000 | ~2-3 horas |
|
||||||
|
| 90.000.000 | 100.000 | ~20-24 horas |
|
||||||
|
|
||||||
|
**Vantagens:**
|
||||||
|
- ✅ Não trava aplicação
|
||||||
|
- ✅ Outras queries conseguem rodar entre os lotes
|
||||||
|
- ✅ Pode pausar (Ctrl+C) e retomar depois (WHERE NULL pega de onde parou)
|
||||||
|
- ✅ Log de progresso em tempo real
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Remoção de Constraints
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Identifica todas as PKs e FKs envolvendo a coluna
|
||||||
|
SELECT name
|
||||||
|
FROM sys.key_constraints
|
||||||
|
WHERE type = 'PK'
|
||||||
|
AND parent_object_id = OBJECT_ID('Transacoes')
|
||||||
|
AND COL_NAME(parent_object_id, parent_column_id) = 'CNPJ_Pagador';
|
||||||
|
|
||||||
|
-- Remove PKs
|
||||||
|
ALTER TABLE Transacoes
|
||||||
|
DROP CONSTRAINT PK_Transacoes_CNPJ;
|
||||||
|
|
||||||
|
-- Remove FKs (tabelas que referenciam)
|
||||||
|
ALTER TABLE Pagamentos
|
||||||
|
DROP CONSTRAINT FK_Pagamentos_Transacoes;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tempo estimado:** ~10 minutos (depende de quantas constraints existem)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4: Column Swap (Renomeação)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Renomeia coluna antiga para _Old
|
||||||
|
EXEC sp_rename 'Transacoes.CNPJ_Pagador', 'CNPJ_Pagador_Old', 'COLUMN';
|
||||||
|
|
||||||
|
-- Renomeia nova coluna para o nome original
|
||||||
|
EXEC sp_rename 'Transacoes.CNPJ_Pagador_New', 'CNPJ_Pagador', 'COLUMN';
|
||||||
|
|
||||||
|
-- Altera para NOT NULL (após validação de 100% preenchido)
|
||||||
|
ALTER TABLE Transacoes
|
||||||
|
ALTER COLUMN CNPJ_Pagador VARCHAR(18) NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tempo estimado:** ~1 segundo (metadata change)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 5: Recriação de Constraints
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Recria PK com nova coluna VARCHAR
|
||||||
|
ALTER TABLE Transacoes
|
||||||
|
ADD CONSTRAINT PK_Transacoes_CNPJ
|
||||||
|
PRIMARY KEY CLUSTERED (CNPJ_Pagador);
|
||||||
|
|
||||||
|
-- Recria FKs
|
||||||
|
ALTER TABLE Pagamentos
|
||||||
|
ADD CONSTRAINT FK_Pagamentos_Transacoes
|
||||||
|
FOREIGN KEY (CNPJ_Pagador) REFERENCES Transacoes(CNPJ_Pagador);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tempo estimado:** ~30-60 minutos (depende do volume)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 6: Validação e Limpeza
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Valida que 100% foi migrado
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM Transacoes
|
||||||
|
WHERE CNPJ_Pagador IS NULL OR CNPJ_Pagador = '';
|
||||||
|
|
||||||
|
-- Valida integridade referencial
|
||||||
|
DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS;
|
||||||
|
|
||||||
|
-- Se tudo OK, remove coluna antiga
|
||||||
|
ALTER TABLE Transacoes
|
||||||
|
DROP COLUMN CNPJ_Pagador_Old;
|
||||||
|
|
||||||
|
-- Remove índice temporário
|
||||||
|
DROP INDEX IX_Temp_CNPJ_New ON Transacoes;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customização do Processo CNPJ Fast
|
||||||
|
|
||||||
|
### Diferenças vs. Processo Original
|
||||||
|
|
||||||
|
O processo **CNPJ Fast** original foi **reestruturado** para este cliente:
|
||||||
|
|
||||||
|
**Mudanças principais:**
|
||||||
|
|
||||||
|
| Aspecto | CNPJ Fast Original | Cliente (Customizado) |
|
||||||
|
|---------|-------------------|---------------------|
|
||||||
|
| **Foco** | Aplicações + DB | Apenas DB (sem software próprio) |
|
||||||
|
| **Discovery** | Inventário de apps | Apenas análise de schema |
|
||||||
|
| **Execução** | Múltiplas aplicações | Scripts SQL massivos |
|
||||||
|
| **Batch Size** | 50k-100k | 100k (otimizado para volume) |
|
||||||
|
| **Monitoramento** | Manual + ferramentas | Logs SQL em tempo real |
|
||||||
|
| **Rollback** | Processo complexo | Simples (DROP COLUMN) |
|
||||||
|
|
||||||
|
**Motivo da reestruturação:**
|
||||||
|
- Cliente não tem aplicações próprias (apenas consome dados)
|
||||||
|
- Foco 100% em otimização de banco
|
||||||
|
- Volume muito maior que casos típicos (100M vs ~10M)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`SQL Server` `T-SQL` `Batch Processing` `Performance Tuning` `Database Optimization` `Migration Scripts` `Phased Commits` `Index Optimization` `Constraint Management`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Por que 100k por batch?
|
||||||
|
|
||||||
|
**Testes de performance:**
|
||||||
|
|
||||||
|
| Batch Size | Tempo/Batch | Lock Duration | Contenção |
|
||||||
|
|------------|-------------|---------------|-----------|
|
||||||
|
| 10.000 | 2s | Baixo | ✅ Mínimo |
|
||||||
|
| 50.000 | 8s | Médio | ✅ Aceitável |
|
||||||
|
| **100.000** | 15s | **Médio** | **✅ Balanceado** |
|
||||||
|
| 500.000 | 90s | Alto | ❌ Impacto em prod |
|
||||||
|
| 1.000.000 | 180s | Muito alto | ❌ Inaceitável |
|
||||||
|
|
||||||
|
**Escolha:** 100k oferece melhor balanço entre performance e impacto.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Por que criar coluna no FINAL?
|
||||||
|
|
||||||
|
**SQL Server internals:**
|
||||||
|
- Adicionar coluna no final = metadata change (rápido)
|
||||||
|
- Adicionar no meio = reescrita de páginas (lento)
|
||||||
|
- Para tabelas grandes, posição importa
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Por que WAITFOR DELAY de 1 segundo?
|
||||||
|
|
||||||
|
**Sem delay:**
|
||||||
|
- ❌ Batch processing consome 100% do I/O
|
||||||
|
- ❌ Queries de aplicação ficam lentas
|
||||||
|
- ❌ Lock escalation pode ocorrer
|
||||||
|
|
||||||
|
**Com delay de 1s:**
|
||||||
|
- ✅ Outras queries têm janela para executar
|
||||||
|
- ✅ I/O distribuído
|
||||||
|
- ✅ Experiência do usuário preservada
|
||||||
|
|
||||||
|
**Trade-off:** Migração leva +1s por batch (~25% mais lenta), mas sistema permanece responsivo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current Status & Next Steps
|
||||||
|
|
||||||
|
### Status Atual (Dezembro 2024)
|
||||||
|
|
||||||
|
📝 **Fase de Preparação:**
|
||||||
|
- ✅ Discovery completo (100M registros identificados)
|
||||||
|
- ✅ Scripts de migração desenvolvidos
|
||||||
|
- ✅ Testes em ambiente de homologação
|
||||||
|
- 🔄 Validação de performance
|
||||||
|
- ⏳ Aguardando janela de manutenção para produção
|
||||||
|
|
||||||
|
### Próximos Passos
|
||||||
|
|
||||||
|
1. **Backup completo** de produção
|
||||||
|
2. **Execução em produção** (ambiente 24/7)
|
||||||
|
3. **Monitoramento em tempo real** durante migração
|
||||||
|
4. **Validação pós-migração** (integridade, performance)
|
||||||
|
5. **Documentação de lessons learned**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned (Até Agora)
|
||||||
|
|
||||||
|
### 1. Experiência Anterior Vale Ouro
|
||||||
|
|
||||||
|
Decisão de usar phased commits veio de **experiência prática** em projetos anteriores, não de documentação ou IA.
|
||||||
|
|
||||||
|
**Situações similares anteriores:**
|
||||||
|
- Migração de dados em e-commerce (50M registros)
|
||||||
|
- Conversão de encoding (UTF-8 em 100M+ rows)
|
||||||
|
- Particionamento de tabelas históricas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. "Measure Twice, Cut Once"
|
||||||
|
|
||||||
|
Antes de executar em produção:
|
||||||
|
- ✅ Testes exaustivos em homologação
|
||||||
|
- ✅ Scripts validados e revisados
|
||||||
|
- ✅ Rollback testado
|
||||||
|
- ✅ Estimativas de tempo confirmadas
|
||||||
|
|
||||||
|
**Tempo de preparação:** 3 semanas
|
||||||
|
**Tempo de execução:** Estimado em 48 horas
|
||||||
|
|
||||||
|
**Ratio:** 10:1 (preparação vs execução)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Customização > One-Size-Fits-All
|
||||||
|
|
||||||
|
O processo CNPJ Fast original precisou ser **reestruturado** para este cliente.
|
||||||
|
|
||||||
|
**Lição:** Processos devem ser:
|
||||||
|
- Estruturados o suficiente para repetir
|
||||||
|
- Flexíveis o suficiente para adaptar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Monitoramento é Crucial
|
||||||
|
|
||||||
|
Scripts com **log detalhado** de progresso permitem:
|
||||||
|
- Estimar tempo restante
|
||||||
|
- Identificar gargalos
|
||||||
|
- Pausar/retomar com confiança
|
||||||
|
- Reportar status para stakeholders
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Log example
|
||||||
|
Processed: 10.000.000 rows. Batch: 100.000
|
||||||
|
Elapsed time: 3600 seconds (10% complete, ~9h remaining)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
### Otimizações Implementadas
|
||||||
|
|
||||||
|
1. **Índice temporário WHERE NULL**
|
||||||
|
- Acelera lookup de registros não migrados
|
||||||
|
- Removido após conclusão
|
||||||
|
|
||||||
|
2. **Batch size otimizado**
|
||||||
|
- Balanceado entre performance e lock time
|
||||||
|
|
||||||
|
3. **Transaction log management**
|
||||||
|
```sql
|
||||||
|
-- Verificar crescimento do log
|
||||||
|
DBCC SQLPERF(LOGSPACE);
|
||||||
|
|
||||||
|
-- Ajustar recovery model (se permitido)
|
||||||
|
ALTER DATABASE MyDatabase SET RECOVERY SIMPLE;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Execução em horário de menor carga**
|
||||||
|
- Janela de manutenção noturna
|
||||||
|
- Final de semana (se possível)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado esperado:** Migração de 100 milhões de registros em ~48 horas, sem downtime significativo e com possibilidade de rollback rápido.
|
||||||
|
|
||||||
|
[Precisa migrar volumes massivos de dados? Entre em contato](#contact)
|
||||||
588
Content/Cases/pt/industrial-learning-platform.md
Normal file
@ -0,0 +1,588 @@
|
|||||||
|
---
|
||||||
|
title: "Plataforma de Treinamento Industrial - De Wireframes a Sistema Completo"
|
||||||
|
slug: "industrial-learning-platform"
|
||||||
|
summary: "Solution Design para plataforma de microlearning em empresa de gases industriais. Identificação de requisitos críticos não mapeados (admin, cadastros, exportação) antes da apresentação ao cliente, evitando retrabalho e garantindo usabilidade real."
|
||||||
|
client: "Empresa de Gases Industriais"
|
||||||
|
industry: "Industrial & Manufatura"
|
||||||
|
timeline: "4 meses"
|
||||||
|
role: "Solution Architect & Tech Lead"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- Solution Design
|
||||||
|
- EdTech
|
||||||
|
- Learning Platform
|
||||||
|
- Requirements Analysis
|
||||||
|
- Tech Lead
|
||||||
|
- User Stories
|
||||||
|
- .NET
|
||||||
|
- Product Design
|
||||||
|
featured: true
|
||||||
|
order: 5
|
||||||
|
date: 2024-06-01
|
||||||
|
seo_title: "Plataforma de Treinamento Industrial - Solution Design"
|
||||||
|
seo_description: "Case de Solution Design para plataforma de microlearning, identificando requisitos críticos antes da apresentação ao cliente e liderando desenvolvimento até produção."
|
||||||
|
seo_keywords: "solution design, learning platform, microlearning, requirements analysis, tech lead, industrial training"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Empresa de gases industriais solicita plataforma para treinar funcionários usando metodologia de **microlearning** (conteúdos curtos e objetivos).
|
||||||
|
|
||||||
|
**Requisito inicial:** "Queremos apenas a estrutura - trilha, microlearning, pergunta de teste e pontuação."
|
||||||
|
|
||||||
|
**Problema:** Especificação incompleta que resultaria em sistema **impossível de usar** (sem forma de cadastrar conteúdo, sem administradores, sem exportar resultados).
|
||||||
|
|
||||||
|
**Solução:** Análise crítica de requisitos **antes da apresentação ao cliente**, identificando gaps funcionais e propondo solução completa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Wireframes Bonitos, Funcionalidade Incompleta
|
||||||
|
|
||||||
|
**Situação inicial:**
|
||||||
|
|
||||||
|
UX criou wireframes lindos mostrando:
|
||||||
|
- ✅ Trilhas de aprendizado
|
||||||
|
- ✅ Microlearnings (vídeo/texto + imagem)
|
||||||
|
- ✅ Perguntas de teste (múltipla escolha)
|
||||||
|
- ✅ Pontuação por funcionário
|
||||||
|
|
||||||
|
**Problema identificado:**
|
||||||
|
|
||||||
|
Ninguém (cliente, UX, comercial) pensou em:
|
||||||
|
|
||||||
|
❌ **Como conteúdo entra no sistema?**
|
||||||
|
- Quem cadastra trilhas?
|
||||||
|
- Quem cria microlearnings?
|
||||||
|
- Quem escreve perguntas?
|
||||||
|
- Interface manual ou import?
|
||||||
|
|
||||||
|
❌ **Quem gerencia o sistema?**
|
||||||
|
- Existe conceito de admin?
|
||||||
|
- RH pode criar admins?
|
||||||
|
- Gestor de área pode ver apenas seu time?
|
||||||
|
|
||||||
|
❌ **Como dados saem do sistema?**
|
||||||
|
- RH precisa de relatórios
|
||||||
|
- Compliance precisa de evidências
|
||||||
|
- Como exportar dados?
|
||||||
|
- Formato: Excel? PDF? API?
|
||||||
|
|
||||||
|
**Risco real:**
|
||||||
|
|
||||||
|
Se desenvolvêssemos exatamente o que foi pedido:
|
||||||
|
- Sistema funcionaria tecnicamente ✅
|
||||||
|
- **Mas seria completamente inutilizável** ❌
|
||||||
|
- Cliente teria que pagar refação para incluir CRUD básico
|
||||||
|
- Retrabalho + custo adicional + frustração
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Design Process
|
||||||
|
|
||||||
|
### Etapa 1: Análise Crítica (Antes da Apresentação)
|
||||||
|
|
||||||
|
**Ação tomada:** Chamei reunião com UX **antes** de apresentar ao cliente.
|
||||||
|
|
||||||
|
**Pontos levantados:**
|
||||||
|
|
||||||
|
**"Como o primeiro conteúdo entra no sistema?"**
|
||||||
|
- UX: "Ah... não pensamos nisso. Vocês vão popular o banco?"
|
||||||
|
- Eu: "E quando cliente quiser adicionar nova trilha? Vamos alterar banco em produção?"
|
||||||
|
|
||||||
|
**"Quem é o dono do sistema?"**
|
||||||
|
- UX: "O RH, imagino."
|
||||||
|
- Eu: "Apenas uma pessoa? E se ela sair da empresa? Como ela delega?"
|
||||||
|
|
||||||
|
**"RH pediu relatórios?"**
|
||||||
|
- UX: "Não foi mencionado no briefing."
|
||||||
|
- Eu: "RH sempre precisa de relatórios. É para compliance (NR, ISO)."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Etapa 2: Requisitos Funcionais Identificados
|
||||||
|
|
||||||
|
Propus 4 módulos adicionais **essenciais**:
|
||||||
|
|
||||||
|
#### 1. Sistema de Administração
|
||||||
|
|
||||||
|
**Funcionalidades:**
|
||||||
|
- Usuário padrão: Apenas faz treinamentos
|
||||||
|
- Usuário admin: Gerencia conteúdo + vê relatórios
|
||||||
|
- Admin pode promover outros usuários a admin
|
||||||
|
- Controle de acesso (admin geral vs admin de área)
|
||||||
|
|
||||||
|
**Por que é crítico:**
|
||||||
|
Sem isso, sistema é estático (conteúdo nunca atualiza).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. CRUD de Conteúdo
|
||||||
|
|
||||||
|
**a) Cadastro de Trilhas:**
|
||||||
|
- Nome da trilha
|
||||||
|
- Descrição
|
||||||
|
- Ordem dos microlearnings
|
||||||
|
- Trilha ativa/inativa (permite despublicar)
|
||||||
|
|
||||||
|
**b) Cadastro de Microlearnings:**
|
||||||
|
- Título
|
||||||
|
- Tipo: Texto simples (2 parágrafos) OU Vídeo
|
||||||
|
- Upload de imagem (se texto)
|
||||||
|
- URL de vídeo (se vídeo)
|
||||||
|
- Ordem dentro da trilha
|
||||||
|
|
||||||
|
**c) Cadastro de Perguntas:**
|
||||||
|
- Pergunta (texto)
|
||||||
|
- 3 opções de resposta:
|
||||||
|
- "Ótimo" (verde)
|
||||||
|
- "Mais ou menos" (amarelo)
|
||||||
|
- "Ruim" (vermelho)
|
||||||
|
- Pontuação por resposta (ex: 10, 5, 0 pontos)
|
||||||
|
- Feedback customizado por resposta
|
||||||
|
|
||||||
|
**Por que é crítico:**
|
||||||
|
Cliente precisa atualizar conteúdo sem chamar dev/DBA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Exportação de Dados
|
||||||
|
|
||||||
|
**Funcionalidades:**
|
||||||
|
- Exportar para Excel (.xlsx)
|
||||||
|
- Filtros:
|
||||||
|
- Por período (data início/fim)
|
||||||
|
- Por trilha
|
||||||
|
- Por funcionário
|
||||||
|
- Por área/departamento
|
||||||
|
- Colunas exportadas:
|
||||||
|
- Nome do funcionário
|
||||||
|
- Matrícula
|
||||||
|
- Trilha concluída
|
||||||
|
- Pontuação total
|
||||||
|
- Data de conclusão
|
||||||
|
- Respostas individuais (para auditoria)
|
||||||
|
|
||||||
|
**Por que é crítico:**
|
||||||
|
RH precisa evidenciar treinamento para:
|
||||||
|
- Normas Regulamentadoras (NR-13, NR-20 - gases inflamáveis)
|
||||||
|
- Auditorias ISO
|
||||||
|
- Processos trabalhistas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. Gestão de Usuários
|
||||||
|
|
||||||
|
**Funcionalidades:**
|
||||||
|
- Importar funcionários (upload CSV/Excel)
|
||||||
|
- Cadastro manual
|
||||||
|
- Ativar/desativar usuários
|
||||||
|
- Atribuir trilhas obrigatórias por área
|
||||||
|
- Notificações de pendências
|
||||||
|
|
||||||
|
**Por que é crítico:**
|
||||||
|
Empresa tem 500+ funcionários, cadastro manual é inviável.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Etapa 3: Apresentação ao Cliente
|
||||||
|
|
||||||
|
**Abordagem:**
|
||||||
|
|
||||||
|
1. Mostrei wireframes do UX (interface bonita)
|
||||||
|
2. Perguntei: "Como vocês vão cadastrar a primeira trilha?"
|
||||||
|
3. Cliente: "Ah... boa pergunta. Não tínhamos pensado nisso."
|
||||||
|
4. Apresentei os 4 módulos adicionais
|
||||||
|
5. Cliente: "Faz total sentido! Sem isso não conseguimos usar."
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
- Proposta aprovada **com módulos adicionais**
|
||||||
|
- Escopo ajustado (timeline + orçamento)
|
||||||
|
- Zero retrabalho futuro
|
||||||
|
- Cliente reconheceu valor agregado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Meu Papel no Projeto
|
||||||
|
|
||||||
|
**1. Solution Architect**
|
||||||
|
- Identificação de requisitos não-funcionais
|
||||||
|
- Desenho de arquitetura (módulos, integrações)
|
||||||
|
- Definição de tecnologias
|
||||||
|
|
||||||
|
**2. Tech Lead**
|
||||||
|
- Liderança técnica da equipe (3 devs)
|
||||||
|
- Code review
|
||||||
|
- Definição de padrões de código
|
||||||
|
- Gestão de riscos técnicos
|
||||||
|
|
||||||
|
**3. Product Owner Técnico**
|
||||||
|
- Criação de **user stories** completas
|
||||||
|
- Priorização de backlog
|
||||||
|
- Refinamento contínuo com cliente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stack Técnico Escolhido
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `.NET 7` - APIs REST
|
||||||
|
- `Entity Framework Core` - ORM
|
||||||
|
- `SQL Server` - Banco de dados
|
||||||
|
- `ClosedXML` - Geração de Excel
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `React` - Interface web
|
||||||
|
- `Material-UI` - Componentes
|
||||||
|
- `React Player` - Player de vídeo
|
||||||
|
- `Chart.js` - Gráficos de progresso
|
||||||
|
|
||||||
|
**Infraestrutura:**
|
||||||
|
- `Azure App Service` - Hospedagem
|
||||||
|
- `Azure Blob Storage` - Armazenamento de vídeos/imagens
|
||||||
|
- `Azure SQL Database` - Banco gerenciado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Stories Criadas
|
||||||
|
|
||||||
|
Escrevi **32 user stories** cobrindo todos os fluxos. Exemplos:
|
||||||
|
|
||||||
|
**US-01: Cadastrar Trilha (Admin)**
|
||||||
|
```
|
||||||
|
Como administrador do sistema
|
||||||
|
Quero cadastrar uma nova trilha de treinamento
|
||||||
|
Para que funcionários possam realizar os cursos
|
||||||
|
|
||||||
|
Critérios de aceitação:
|
||||||
|
- Admin acessa menu "Trilhas" → "Nova Trilha"
|
||||||
|
- Preenche: Nome, Descrição, Status (Ativa/Inativa)
|
||||||
|
- Pode adicionar microlearnings existentes à trilha
|
||||||
|
- Define ordem dos microlearnings (drag & drop)
|
||||||
|
- Sistema valida campos obrigatórios
|
||||||
|
- Salva e exibe mensagem de sucesso
|
||||||
|
```
|
||||||
|
|
||||||
|
**US-15: Realizar Microlearning (Funcionário)**
|
||||||
|
```
|
||||||
|
Como funcionário
|
||||||
|
Quero realizar um microlearning da minha trilha
|
||||||
|
Para aprender sobre o tema e ganhar pontos
|
||||||
|
|
||||||
|
Critérios de aceitação:
|
||||||
|
- Funcionário acessa trilha atribuída
|
||||||
|
- Vê lista de microlearnings (não completados primeiro)
|
||||||
|
- Clica em microlearning → abre tela com:
|
||||||
|
- Texto (2 parágrafos) + Imagem OU
|
||||||
|
- Player de vídeo embarcado
|
||||||
|
- Botão "Continuar" aparece após:
|
||||||
|
- 30s (se texto)
|
||||||
|
- Final do vídeo (se vídeo)
|
||||||
|
- Marca microlearning como visto
|
||||||
|
- Pergunta de teste aparece automaticamente
|
||||||
|
```
|
||||||
|
|
||||||
|
**US-22: Exportar Resultados (Admin)**
|
||||||
|
```
|
||||||
|
Como administrador
|
||||||
|
Quero exportar resultados de treinamento para Excel
|
||||||
|
Para gerar relatórios de compliance e auditorias
|
||||||
|
|
||||||
|
Critérios de aceitação:
|
||||||
|
- Admin acessa "Relatórios" → "Exportar"
|
||||||
|
- Seleciona filtros (período, trilha, área)
|
||||||
|
- Clica "Gerar Excel"
|
||||||
|
- Sistema processa e baixa arquivo .xlsx
|
||||||
|
- Excel contém colunas: Nome, Matrícula, Trilha, Pontos, Data, Respostas
|
||||||
|
- Formato legível (headers em negrito, colunas autoajustadas)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features Implemented
|
||||||
|
|
||||||
|
### 1. Sistema de Pontuação Gamificado
|
||||||
|
|
||||||
|
**Mecânica:**
|
||||||
|
- Cada pergunta vale pontos (configurável)
|
||||||
|
- Resposta "Ótimo": 10 pontos
|
||||||
|
- Resposta "Mais ou menos": 5 pontos
|
||||||
|
- Resposta "Ruim": 0 pontos
|
||||||
|
|
||||||
|
**Dashboard do funcionário:**
|
||||||
|
- Pontuação total
|
||||||
|
- Ranking (opcional, configurável)
|
||||||
|
- Badges por trilhas concluídas
|
||||||
|
- Progresso visual (barra de %)
|
||||||
|
|
||||||
|
**Por que funciona:**
|
||||||
|
Funcionários de chão de fábrica engajam mais com elementos de gamificação.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Microlearning Adaptativo
|
||||||
|
|
||||||
|
**Tipos de conteúdo:**
|
||||||
|
|
||||||
|
**Texto + Imagem:**
|
||||||
|
- 2 parágrafos (máx 300 palavras)
|
||||||
|
- 1 imagem ilustrativa
|
||||||
|
- Ideal para: Procedimentos, normas, conceitos
|
||||||
|
|
||||||
|
**Vídeo:**
|
||||||
|
- Vídeos curtos (2-5 min)
|
||||||
|
- Player embarcado (YouTube/Vimeo ou upload)
|
||||||
|
- Ideal para: Demonstrações, operações de equipamento
|
||||||
|
|
||||||
|
**Por que microlearning?**
|
||||||
|
- Funcionários fazem no intervalo (10-15min)
|
||||||
|
- Conteúdo curto = maior retenção
|
||||||
|
- Facilita atualização (vs cursos longos)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Sistema de Administração Delegada
|
||||||
|
|
||||||
|
**Hierarquia:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Admin Geral (RH)
|
||||||
|
↓ pode promover
|
||||||
|
Admin de Área (Gerentes)
|
||||||
|
↓ pode visualizar apenas
|
||||||
|
Funcionários da sua área
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permissões:**
|
||||||
|
- Admin geral: Cria trilhas, promove admins, vê todos os dados
|
||||||
|
- Admin de área: Vê apenas relatórios da sua área
|
||||||
|
- Funcionário: Apenas realiza treinamentos
|
||||||
|
|
||||||
|
**Auditoria:**
|
||||||
|
- Logs de quem criou/editou cada conteúdo
|
||||||
|
- Histórico de promoções a admin
|
||||||
|
- Compliance SOX/ISO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Exportação para Compliance
|
||||||
|
|
||||||
|
**Formato do Excel gerado:**
|
||||||
|
|
||||||
|
| Matrícula | Nome | Área | Trilha | Data Conclusão | Pontos | Status |
|
||||||
|
|-----------|------|------|--------|----------------|--------|--------|
|
||||||
|
| 1001 | João Silva | Produção | Segurança NR-20 | 15/11/2024 | 95/100 | ✅ Aprovado |
|
||||||
|
| 1002 | Maria Santos | Logística | Manuseio Gases | 14/11/2024 | 78/100 | ✅ Aprovado |
|
||||||
|
|
||||||
|
**Aba adicional: Detalhamento de Respostas**
|
||||||
|
- Permite auditoria: "Funcionário X acertou pergunta Y?"
|
||||||
|
- Evidência para processos trabalhistas
|
||||||
|
- Compliance NR-13/NR-20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Sistema em Produção
|
||||||
|
|
||||||
|
**Status atual:** Em uso há 4+ meses
|
||||||
|
|
||||||
|
**Métricas de adoção:**
|
||||||
|
- 👥 500+ funcionários cadastrados
|
||||||
|
- 📚 12 trilhas ativas
|
||||||
|
- 📖 150+ microlearnings criados
|
||||||
|
- ✅ 8.000+ treinamentos concluídos
|
||||||
|
- 📊 100+ relatórios exportados (compliance)
|
||||||
|
|
||||||
|
**Taxa de conclusão:** 87% (média indústria: 45%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto no Cliente
|
||||||
|
|
||||||
|
**Antes:**
|
||||||
|
- Treinamentos presenciais (custo alto, agenda difícil)
|
||||||
|
- Evidências em papel (perdas, difícil auditoria)
|
||||||
|
- Dificuldade em atualizar conteúdo
|
||||||
|
|
||||||
|
**Depois:**
|
||||||
|
- Treinamento assíncrono (funcionário faz quando pode)
|
||||||
|
- Evidências digitais (compliance facilitado)
|
||||||
|
- RH atualiza conteúdo sem chamar TI
|
||||||
|
- Redução de 70% no custo de treinamento
|
||||||
|
|
||||||
|
**Feedback do cliente:**
|
||||||
|
> "Se tivéssemos implementado apenas o que pedimos inicialmente, o sistema seria inútil. A análise prévia salvou o projeto."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Valor do Solution Design
|
||||||
|
|
||||||
|
**ROI da análise pré-venda:**
|
||||||
|
|
||||||
|
**Cenário A (sem análise):**
|
||||||
|
1. Desenvolver apenas interface (2 meses)
|
||||||
|
2. Cliente testa e percebe que falta CRUD (1 mês depois)
|
||||||
|
3. Refação para adicionar módulos (2+ meses)
|
||||||
|
4. **Total: 5+ meses + frustração do cliente**
|
||||||
|
|
||||||
|
**Cenário B (com análise - o que fizemos):**
|
||||||
|
1. Identificar requisitos antes (1 semana)
|
||||||
|
2. Aprovar escopo completo (1 semana)
|
||||||
|
3. Desenvolver solução correta (4 meses)
|
||||||
|
4. **Total: 4 meses + cliente satisfeito**
|
||||||
|
|
||||||
|
**Economia:** 1+ mês de retrabalho + custo de oportunidade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`.NET 7` `C#` `Entity Framework Core` `SQL Server` `React` `Material-UI` `Azure App Service` `Azure Blob Storage` `ClosedXML` `Chart.js` `User Stories` `Solution Design` `Tech Lead`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Por que não usar LMS pronto? (Moodle, Canvas)
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
1. ❌ Moodle (open-source, gratuito)
|
||||||
|
2. ❌ Totara/Canvas (LMS corporativo)
|
||||||
|
3. ✅ **Desenvolvimento custom**
|
||||||
|
|
||||||
|
**Justificativa:**
|
||||||
|
- LMS genérico: Complexidade desnecessária (fóruns, wikis, etc)
|
||||||
|
- Cliente quer **apenas microlearning** (simplicidade)
|
||||||
|
- Custo de licença LMS > custo de dev custom
|
||||||
|
- Integração com AD/SSO do cliente (mais fácil custom)
|
||||||
|
- UX otimizada para chão de fábrica (mobile-first, touch)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Por que 3 opções de resposta (vs 4-5)?
|
||||||
|
|
||||||
|
**Escolha:** Verde (Ótimo), Amarelo (Mais ou menos), Vermelho (Ruim)
|
||||||
|
|
||||||
|
**Justificativa:**
|
||||||
|
- Funcionários de chão de fábrica preferem simplicidade
|
||||||
|
- Cores universais (semáforo)
|
||||||
|
- Evita paradoxo da escolha (menos opções = mais engajamento)
|
||||||
|
- Gamificação mais clara
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Por que Export Excel (vs Dashboard online)?
|
||||||
|
|
||||||
|
**Ambos foram implementados**, mas Excel é crítico para:
|
||||||
|
|
||||||
|
**Compliance regulatório:**
|
||||||
|
- Auditores pedem "arquivo assinado digitalmente"
|
||||||
|
- NR-13/NR-20 exigem evidência física
|
||||||
|
- Processos trabalhistas aceitam Excel
|
||||||
|
|
||||||
|
**Flexibilidade:**
|
||||||
|
- RH pode fazer análises customizadas no Excel
|
||||||
|
- Combinar com outras fontes de dados
|
||||||
|
- Apresentações para diretoria
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Solution Design Previne Retrabalho
|
||||||
|
|
||||||
|
**Lição:** 1 semana de análise crítica economiza meses de refação.
|
||||||
|
|
||||||
|
**Aplicação:**
|
||||||
|
- Sempre questionar especificações incompletas
|
||||||
|
- Pensar no "dia seguinte" (quem gerencia isso em produção?)
|
||||||
|
- Envolver cliente em discussões de requisitos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. UX ≠ Requisitos Funcionais
|
||||||
|
|
||||||
|
**Lição:** Wireframes bonitos não substituem análise de requisitos.
|
||||||
|
|
||||||
|
**UX foca em:** Como usuário **usa** o sistema
|
||||||
|
**Solution Design foca em:** Como sistema **funciona** end-to-end
|
||||||
|
|
||||||
|
Ambos são necessários e complementares.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Perguntar "Como?" é Mais Importante que "O Quê?"
|
||||||
|
|
||||||
|
**Cliente diz:** "Quero trilhas e microlearnings"
|
||||||
|
**Solution Designer pergunta:** "Como a primeira trilha entra no sistema?"
|
||||||
|
|
||||||
|
Essa pergunta simples revelou 4 módulos faltantes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. User Stories Bem Escritas Aceleram Desenvolvimento
|
||||||
|
|
||||||
|
**Investimento:** 2 semanas escrevendo 32 user stories detalhadas
|
||||||
|
|
||||||
|
**Retorno:**
|
||||||
|
- Devs sabiam exatamente o que construir
|
||||||
|
- Zero ambiguidade
|
||||||
|
- Pouquíssimos bugs (requisitos claros)
|
||||||
|
- Cliente validou histórias antes de codificar
|
||||||
|
|
||||||
|
**Lição:** Tempo gasto em planejamento reduz tempo de desenvolvimento.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Compliance é Requisito Oculto
|
||||||
|
|
||||||
|
**Em indústrias reguladas** (saúde, energia, químico), sempre haverá:
|
||||||
|
- Necessidade de auditoria
|
||||||
|
- Exportação de evidências
|
||||||
|
- Logs de quem fez o quê
|
||||||
|
|
||||||
|
**Lição:** Perguntar sobre compliance **antes**, não depois.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenges Overcome
|
||||||
|
|
||||||
|
| Desafio | Solução | Resultado |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| Especificação incompleta | Análise crítica pré-venda | Escopo correto desde início |
|
||||||
|
| Cliente sem conhecimento técnico | User stories em linguagem de negócio | Cliente validou requisitos |
|
||||||
|
| Funcionários com baixa familiaridade digital | UX simplificado (3 botões, cores) | 87% taxa de conclusão |
|
||||||
|
| Compliance NR-13/NR-20 | Export Excel com detalhamento | Aprovado em 2 auditorias |
|
||||||
|
| Gestão de 500+ usuários | Import CSV + hierarquia de admins | Onboarding em 1 semana |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Roadmap Futuro)
|
||||||
|
|
||||||
|
**Funcionalidades planejadas:**
|
||||||
|
|
||||||
|
1. **Notificações Push**
|
||||||
|
- Lembrar funcionário de treinamento pendente
|
||||||
|
- Avisar de nova trilha obrigatória
|
||||||
|
|
||||||
|
2. **App Mobile Nativo**
|
||||||
|
- Offline-first (vídeos baixados)
|
||||||
|
- Funcionários sem computador
|
||||||
|
|
||||||
|
3. **Certificados Digitais**
|
||||||
|
- PDF assinado digitalmente
|
||||||
|
- QR code para validação
|
||||||
|
|
||||||
|
4. **Inteligência de Dados**
|
||||||
|
- Quais microlearnings têm mais erro?
|
||||||
|
- Identificar gaps de conhecimento por área
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** Sistema funcional em produção, cliente satisfeito, zero retrabalho - tudo porque 1 semana foi investida em **pensar antes de codificar**.
|
||||||
|
|
||||||
|
[Precisa de análise crítica de requisitos? Entre em contato](#contact)
|
||||||
577
Content/Cases/pt/pharma-digital-transformation.md
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
---
|
||||||
|
title: "MVP Digital para Laboratório Farmacêutico - Do Zero à Produção"
|
||||||
|
slug: "pharma-digital-transformation"
|
||||||
|
summary: "Liderança de squad em projeto greenfield para laboratório farmacêutico, construindo MVP de plataforma digital com integrações complexas (Salesforce, Twilio, APIs oficiais) partindo do zero absoluto - sem Git, sem servidores, sem infraestrutura."
|
||||||
|
client: "Laboratório Farmacêutico"
|
||||||
|
industry: "Farmacêutica & Saúde"
|
||||||
|
timeline: "4 meses (2 meses de atraso planejado)"
|
||||||
|
role: "Tech Lead & Solution Architect"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- MVP
|
||||||
|
- Digital Transformation
|
||||||
|
- .NET
|
||||||
|
- React
|
||||||
|
- Next.js
|
||||||
|
- Salesforce
|
||||||
|
- Twilio
|
||||||
|
- SQL Server
|
||||||
|
- Tech Lead
|
||||||
|
- Greenfield
|
||||||
|
featured: true
|
||||||
|
order: 3
|
||||||
|
date: 2023-03-01
|
||||||
|
seo_title: "MVP Digital Farmacêutico - Transformação Digital do Zero"
|
||||||
|
seo_description: "Case de construção de MVP digital para laboratório farmacêutico partindo do zero: sem Git, sem infraestrutura, com integrações complexas e entrega bem-sucedida."
|
||||||
|
seo_keywords: "MVP, digital transformation, pharma, .NET, React, Next.js, Salesforce, greenfield project, tech lead"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Laboratório farmacêutico no **início da transformação digital** contrata consultoria para construir plataforma de descontos para médicos prescritores, partindo de protótipo em WordPress.
|
||||||
|
|
||||||
|
**Desafio único:** Começar projeto greenfield em empresa **sem infraestrutura básica** de desenvolvimento - sem Git, sem servidores provisionados, sem processos definidos.
|
||||||
|
|
||||||
|
**Contexto:** Projeto executado em ambiente de múltiplas squads. **Entrega bem-sucedida em produção** apesar dos desafios iniciais de infraestrutura, com atraso controlado de 2 meses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
### Transformação Digital... Partindo do Zero Absoluto
|
||||||
|
|
||||||
|
**Estado inicial da empresa (2023):**
|
||||||
|
|
||||||
|
❌ **Sem Git/versionamento**
|
||||||
|
- Código apenas em máquinas locais
|
||||||
|
- Histórico inexistente
|
||||||
|
- Colaboração impossível
|
||||||
|
|
||||||
|
❌ **Sem servidores provisionados**
|
||||||
|
- Ambiente de desenvolvimento inexistente
|
||||||
|
- Homologação não configurada
|
||||||
|
- Produção não preparada
|
||||||
|
|
||||||
|
❌ **Sem processos de desenvolvimento**
|
||||||
|
- Sem CI/CD
|
||||||
|
- Sem code review
|
||||||
|
- Sem gestão de tarefas estruturada
|
||||||
|
|
||||||
|
❌ **Sem equipe técnica interna experiente**
|
||||||
|
- Time sem familiaridade com stacks modernas
|
||||||
|
- Primeiro contato com React, APIs REST
|
||||||
|
- Inexperiência com integrações complexas
|
||||||
|
|
||||||
|
**Ponto de partida técnico:**
|
||||||
|
- Protótipo funcional em **WordPress**
|
||||||
|
- Conteúdo e textos já aprovados
|
||||||
|
- UX/UI definido
|
||||||
|
- Regras de negócio documentadas (parcialmente)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Integrações Complexas Requeridas
|
||||||
|
|
||||||
|
O MVP precisava integrar com múltiplos sistemas externos:
|
||||||
|
|
||||||
|
1. 🔐 **Salesforce** - Registro de pedidos de desconto
|
||||||
|
2. 📱 **Twilio** - SMS para validação de login (2FA)
|
||||||
|
3. 🏥 **API oficial de médicos** - Validação de CRM + dados profissionais
|
||||||
|
4. 🎯 **Interplayers** - Envio de registros de desconto por CPF
|
||||||
|
5. 📄 **WordPress** - Leitura de conteúdo (CMS headless)
|
||||||
|
6. 💾 **SQL Server** - Persistência de dados
|
||||||
|
|
||||||
|
**Complexidade adicional:**
|
||||||
|
- Diferentes credenciais/ambientes por integração
|
||||||
|
- SLAs variados (Twilio crítico, WordPress tolerante)
|
||||||
|
- Tratamento de erros específico por provider
|
||||||
|
- Compliance LGPD (dados sensíveis de médicos)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution Architecture
|
||||||
|
|
||||||
|
### Estratégia: Start Small, Build Solid
|
||||||
|
|
||||||
|
**Decisão inicial:** Explicar ao time o processo que iríamos seguir, estabelecendo fundações antes de codificar.
|
||||||
|
|
||||||
|
### Fase 1: Setup de Infraestrutura Básica (Semanas 1-2)
|
||||||
|
|
||||||
|
Mesmo sem servidores provisionados, iniciei setup essencial:
|
||||||
|
|
||||||
|
**Git & Versionamento:**
|
||||||
|
```bash
|
||||||
|
# Repositório estruturado desde dia 1
|
||||||
|
git init
|
||||||
|
git flow init # Branch strategy definida
|
||||||
|
|
||||||
|
# Estrutura de monorepo
|
||||||
|
/
|
||||||
|
├── frontend/ # Next.js + React
|
||||||
|
├── backend/ # .NET APIs
|
||||||
|
├── cms-adapter/ # WordPress integration
|
||||||
|
└── docs/ # Arquitetura e ADRs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processo explicado ao time:**
|
||||||
|
1. ✅ Tudo no Git (commits atômicos, mensagens descritivas)
|
||||||
|
2. ✅ Feature branches (nunca commit direto em main)
|
||||||
|
3. ✅ Code review obrigatório (2 aprovações)
|
||||||
|
4. ✅ CI/CD preparado (para quando servidores estiverem prontos)
|
||||||
|
|
||||||
|
**Ambientes locais primeiro:**
|
||||||
|
- Docker Compose para desenvolvimento local
|
||||||
|
- Mock de APIs externas (até credenciais chegarem)
|
||||||
|
- SQL Server local com seeds de dados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 2: Arquitetura Moderna & Desacoplada
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ FRONTEND (Next.js + React) │
|
||||||
|
│ - SSR para SEO │
|
||||||
|
│ - Client-side para interatividade │
|
||||||
|
│ - Consumo de APIs │
|
||||||
|
└────────────┬────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────┐
|
||||||
|
│ BACKEND APIs (.NET 7) │
|
||||||
|
│ - REST APIs │
|
||||||
|
│ - Authentication/Authorization │
|
||||||
|
│ - Business logic │
|
||||||
|
│ - Orchestration layer │
|
||||||
|
└────┬────┬────┬────┬────┬─────────────────────────┬──┘
|
||||||
|
│ │ │ │ │ │
|
||||||
|
▼ ▼ ▼ ▼ ▼ ▼
|
||||||
|
┌────────┐ ┌──────┐ ┌──────┐ ┌────────┐ ┌────────┐ ┌──────────┐
|
||||||
|
│Salesf. │ │Twilio│ │CRM │ │Interpl.│ │WordPr. │ │SQL Server│
|
||||||
|
│ │ │ │ │API │ │ │ │(CMS) │ │ │
|
||||||
|
└────────┘ └──────┘ └──────┘ └────────┘ └────────┘ └──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stack escolhido:**
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `Next.js 13` - SSR, routing, otimizações
|
||||||
|
- `React 18` - Componentes, hooks, context
|
||||||
|
- `TypeScript` - Type safety
|
||||||
|
- `Tailwind CSS` - Styling moderno
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `.NET 7` - APIs REST
|
||||||
|
- `Entity Framework Core` - ORM
|
||||||
|
- `SQL Server 2019` - Banco de dados
|
||||||
|
- `Polly` - Resilience patterns (retry, circuit breaker)
|
||||||
|
|
||||||
|
**Por que Next.js em vez de manter WordPress?**
|
||||||
|
- ✅ Performance (SSR vs PHP monolítico)
|
||||||
|
- ✅ SEO otimizado (critical para farmacêutica)
|
||||||
|
- ✅ Experiência moderna (SPA quando necessário)
|
||||||
|
- ✅ Escalabilidade
|
||||||
|
- ✅ WordPress mantido apenas como CMS (headless)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 3: Integrações (Core do Projeto)
|
||||||
|
|
||||||
|
#### 1. Salesforce - Campanhas e Registro de Pedidos
|
||||||
|
|
||||||
|
**Solução implementada:**
|
||||||
|
|
||||||
|
O Salesforce foi configurado para gerenciar duas funcionalidades principais:
|
||||||
|
|
||||||
|
**a) Campanhas de desconto:**
|
||||||
|
- Marketing configura campanhas no Salesforce (medicamento X, desconto Y%, período)
|
||||||
|
- Backend consulta campanhas ativas via API
|
||||||
|
- Frontend (Next.js) exibe percentual de desconto disponível baseado em: medicamento + campanha ativa
|
||||||
|
|
||||||
|
**b) Registro de pedidos:**
|
||||||
|
- Usuário informa: CRM do médico, UF, CPF do paciente, medicamento
|
||||||
|
- Sistema valida dados (CRM real via API oficial, CPF válido)
|
||||||
|
- Percentual é calculado automaticamente (campanhas do Salesforce + regras do CMS)
|
||||||
|
- Pedido é registrado no Salesforce com todos os dados (compliance LGPD)
|
||||||
|
|
||||||
|
**Desafios técnicos superados:**
|
||||||
|
- Autenticação OAuth2 com refresh token automático
|
||||||
|
- Rate limiting (Salesforce tem limites de API/dia)
|
||||||
|
- Retry logic para falhas transitórias (Polly)
|
||||||
|
- Mascaramento de CPF para logs (LGPD)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Twilio - Autenticação por SMS (2FA)
|
||||||
|
|
||||||
|
**Solução implementada:**
|
||||||
|
|
||||||
|
Sistema de autenticação de dois fatores para garantir segurança:
|
||||||
|
|
||||||
|
**Fluxo de login:**
|
||||||
|
1. Usuário informa telefone
|
||||||
|
2. Backend gera código de 6 dígitos (válido por 5 minutos)
|
||||||
|
3. SMS enviado via Twilio ("Seu código: 123456")
|
||||||
|
4. Usuário digita código no frontend
|
||||||
|
5. Backend valida código + timestamp de expiração
|
||||||
|
6. Token JWT emitido após validação bem-sucedida
|
||||||
|
|
||||||
|
**Compliance e auditoria:**
|
||||||
|
- Telefones mascarados nos logs (LGPD)
|
||||||
|
- Auditoria completa (quem, quando, qual SMS)
|
||||||
|
- Taxa de entrega: 99.8%
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. API Oficial de Médicos (Conselho Regional de Medicina)
|
||||||
|
|
||||||
|
**Solução implementada:**
|
||||||
|
|
||||||
|
Validação automática de médicos via API oficial dos conselhos de medicina:
|
||||||
|
|
||||||
|
**Validações realizadas:**
|
||||||
|
- CRM existe e está ativo no conselho
|
||||||
|
- Nome do médico corresponde ao CRM informado
|
||||||
|
- Especialidade é permitida (regra de negócio do laboratório)
|
||||||
|
- UF corresponde ao estado de registro
|
||||||
|
|
||||||
|
**Otimizações:**
|
||||||
|
- Cache de 24 horas para reduzir chamadas à API oficial
|
||||||
|
- Fallback em caso de API fora do ar (notifica admin)
|
||||||
|
- Retry automático para falhas transitórias
|
||||||
|
|
||||||
|
**Por que isso importa:**
|
||||||
|
Garante que apenas médicos reais e ativos possam prescrever descontos, evitando fraudes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. WordPress como CMS Headless
|
||||||
|
|
||||||
|
**Solução implementada:**
|
||||||
|
|
||||||
|
Marketing continua gerenciando conteúdo no WordPress (familiar), mas frontend é Next.js moderno.
|
||||||
|
|
||||||
|
**Arquitetura:**
|
||||||
|
- WordPress: Gerencia textos, imagens, regras de campanhas
|
||||||
|
- WordPress REST API: Expõe conteúdo via JSON
|
||||||
|
- Next.js: Consome API e renderiza com SSR (SEO otimizado)
|
||||||
|
|
||||||
|
**Benefícios:**
|
||||||
|
- ✅ Marketing não precisa aprender nova ferramenta
|
||||||
|
- ✅ Frontend moderno (performance, UX)
|
||||||
|
- ✅ SEO otimizado (Server-Side Rendering)
|
||||||
|
- ✅ Separação clara de responsabilidades (conteúdo vs código)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fase 4: Resiliência & Error Handling
|
||||||
|
|
||||||
|
Com múltiplas integrações externas, falhas são inevitáveis. A solução foi implementar **padrões de resiliência** usando biblioteca Polly (.NET):
|
||||||
|
|
||||||
|
**Padrões implementados:**
|
||||||
|
|
||||||
|
**1. Retry (Tentar novamente)**
|
||||||
|
- Se Salesforce/Twilio/CRM API falham, sistema tenta automaticamente 2-3x
|
||||||
|
- Espera cresce exponencialmente (1s, 2s, 4s) para evitar sobrecarga
|
||||||
|
- Apenas erros transitórios (timeout, 503) são retentados
|
||||||
|
|
||||||
|
**2. Circuit Breaker (Disjuntor)**
|
||||||
|
- Se serviço falha 5x seguidas, "abre o circuito" por 30s
|
||||||
|
- Durante 30s, não tenta mais (evita desperdiçar recursos)
|
||||||
|
- Após 30s, tenta novamente (pode ter voltado)
|
||||||
|
|
||||||
|
**3. Timeout**
|
||||||
|
- Cada integração tem tempo máximo de resposta
|
||||||
|
- Evita requisições travadas indefinidamente
|
||||||
|
|
||||||
|
**4. Fallback (Plano B)**
|
||||||
|
- Salesforce fora: Pedido vai para fila, processa depois
|
||||||
|
- Twilio fora: Alerta administrador via email
|
||||||
|
- CRM API fora: Usa cache (dados de 24h atrás)
|
||||||
|
- WordPress fora: Exibe conteúdo estático pré-carregado
|
||||||
|
|
||||||
|
**Estratégias por integração:**
|
||||||
|
|
||||||
|
| Integração | Retry | Circuit Breaker | Timeout | Plano B |
|
||||||
|
|----------|-------|-----------------|---------|----------|
|
||||||
|
| Salesforce | 3x (exponencial) | 5 falhas/30s | 10s | Fila de retry |
|
||||||
|
| Twilio | 2x (linear) | 3 falhas/60s | 5s | Alerta admin |
|
||||||
|
| CRM API | 3x (exponencial) | Não | 15s | Cache |
|
||||||
|
| WordPress | Não | Não | 3s | Conteúdo estático |
|
||||||
|
|
||||||
|
**Resultado em produção:**
|
||||||
|
- Salesforce teve manutenção (1h) → Sistema continuou funcionando (fila processou depois)
|
||||||
|
- Twilio teve instabilidade → Retry automático resolveu 95% dos casos
|
||||||
|
- Zero downtime percebido pelos usuários
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overcoming Infrastructure Challenges
|
||||||
|
|
||||||
|
### Problema: Servidores Não Provisionados
|
||||||
|
|
||||||
|
**Solução temporária:**
|
||||||
|
1. Desenvolvimento 100% local (Docker Compose)
|
||||||
|
2. Mocks de serviços externos (quando credenciais atrasaram)
|
||||||
|
3. CI/CD preparado mas não ativo (aguardando infra)
|
||||||
|
|
||||||
|
**Quando servidores chegaram (semana 6):**
|
||||||
|
- Deploy em 2 horas (já estava preparado)
|
||||||
|
- Zero surpresas (tudo testado localmente)
|
||||||
|
- Rollout suave
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problema: Credenciais de Integração Atrasadas
|
||||||
|
|
||||||
|
**Impacto:** Twilio e Salesforce demoraram 3 semanas para serem provisionadas.
|
||||||
|
|
||||||
|
**Solução:** Criar versões "mock" (simuladas) de cada integração:
|
||||||
|
- Mock do Twilio: Registra no log em vez de enviar SMS real
|
||||||
|
- Mock do Salesforce: Salva pedido em arquivo JSON local
|
||||||
|
- Mock da CRM API: Retorna dados fictícios de médicos
|
||||||
|
|
||||||
|
**Como funciona:**
|
||||||
|
- Ambiente de desenvolvimento: Usa mocks (não precisa de credenciais)
|
||||||
|
- Ambiente de produção: Usa integrações reais (quando credenciais chegarem)
|
||||||
|
- Troca automática baseada em configuração
|
||||||
|
|
||||||
|
**Resultado:** Time continuou 100% produtivo durante 3 semanas, testando fluxos completos sem depender de credenciais.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Problema: Time Inexperiente com Stack Moderno
|
||||||
|
|
||||||
|
**Contexto:** Equipe não tinha experiência com React, TypeScript, .NET Core moderno, APIs REST.
|
||||||
|
|
||||||
|
**Abordagem de capacitação:**
|
||||||
|
|
||||||
|
**1. Pair Programming (1h/dia por desenvolvedor)**
|
||||||
|
- Tech lead trabalha ao lado do dev
|
||||||
|
- Compartilhamento de tela + explicação em tempo real
|
||||||
|
- Dev escreve código, tech lead guia
|
||||||
|
|
||||||
|
**2. Code Review Educativo**
|
||||||
|
- Não apenas "aprovar" ou "reprovar"
|
||||||
|
- Comentários explicam o **porquê** de cada sugestão
|
||||||
|
- Exemplo: "Sempre trate erros de requisições! Se API cair, usuário precisa saber o que aconteceu."
|
||||||
|
|
||||||
|
**3. Documentação Viva**
|
||||||
|
- ADRs (Architecture Decision Records): Por que escolhemos X e não Y?
|
||||||
|
- READMEs: Como rodar, como testar, como deployar
|
||||||
|
- Onboarding guide: Do zero à primeira feature
|
||||||
|
|
||||||
|
**4. Live Coding Semanal (2h)**
|
||||||
|
- Tech lead resolve problema real ao vivo
|
||||||
|
- Time observa processo de pensamento
|
||||||
|
- Q&A ao final
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
- Após 4 semanas, time estava autônomo
|
||||||
|
- Qualidade de código aumentou consistentemente
|
||||||
|
- Devs passaram a fazer code review entre si (peer review)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results & Impact
|
||||||
|
|
||||||
|
### Entrega Bem-Sucedida Apesar dos Desafios
|
||||||
|
|
||||||
|
**Contexto:** Programa com múltiplas squads trabalhando em paralelo.
|
||||||
|
|
||||||
|
**Resultado alcançado:**
|
||||||
|
- ✅ **MVP entregue em produção com sucesso**
|
||||||
|
- ✅ Atraso controlado de 2 meses (significativamente menor que outras iniciativas do programa)
|
||||||
|
- ✅ Todas as integrações funcionando conforme planejado
|
||||||
|
- ✅ Zero critical bugs em produção (primeira semana)
|
||||||
|
|
||||||
|
**Por que a entrega foi bem-sucedida?**
|
||||||
|
|
||||||
|
1. **Setup antecipado** - Git, processos, Docker local desde dia 1
|
||||||
|
2. **Mocks estratégicos** - Time não ficou bloqueado esperando infra
|
||||||
|
3. **Arquitetura sólida** - Resiliência desde o início
|
||||||
|
4. **Upskilling contínuo** - Time aprendeu fazendo
|
||||||
|
5. **Comunicação proativa** - Riscos reportados cedo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Métricas do MVP
|
||||||
|
|
||||||
|
**Performance:**
|
||||||
|
- ⚡ Tempo de carregamento: <2s (95th percentile)
|
||||||
|
- 📱 Lighthouse score: 95+ (mobile)
|
||||||
|
- 🔒 SSL A+ rating
|
||||||
|
|
||||||
|
**Integrações:**
|
||||||
|
- 📊 Salesforce: 100% de pedidos sincronizados
|
||||||
|
- 📱 Twilio: 99.8% delivery rate
|
||||||
|
- 🏥 CRM API: 10k validações/dia (média)
|
||||||
|
- 💾 SQL Server: 50k registros/mês
|
||||||
|
|
||||||
|
**Adoção:**
|
||||||
|
- 👨⚕️ 2.000+ médicos cadastrados (3 primeiros meses)
|
||||||
|
- 🎯 15.000+ pedidos de desconto processados
|
||||||
|
- ⭐ 4.8/5 satisfação (pesquisa interna)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Impacto no Cliente
|
||||||
|
|
||||||
|
**Transformação digital iniciada:**
|
||||||
|
- ✅ Git implementado e adotado
|
||||||
|
- ✅ Processos de desenvolvimento estabelecidos
|
||||||
|
- ✅ Time interno capacitado em stacks modernas
|
||||||
|
- ✅ Infraestrutura cloud configurada (Azure)
|
||||||
|
- ✅ Roadmap de evolução definido
|
||||||
|
|
||||||
|
**Base para futuros projetos:**
|
||||||
|
- Arquitetura serviu de referência para outras iniciativas
|
||||||
|
- Padrões de código documentados (coding standards)
|
||||||
|
- Pipelines CI/CD reutilizados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`.NET 7` `C#` `Entity Framework Core` `SQL Server` `React 18` `Next.js 13` `TypeScript` `Tailwind CSS` `Salesforce API` `Twilio` `WordPress REST API` `Docker` `Polly` `OAuth2` `JWT` `LGPD Compliance`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions & Trade-offs
|
||||||
|
|
||||||
|
### Por que Next.js em vez de React puro?
|
||||||
|
|
||||||
|
**Requisitos:**
|
||||||
|
- SEO crítico (farmacêutica precisa rankar)
|
||||||
|
- Performance (médicos usam mobile)
|
||||||
|
- Conteúdo dinâmico (WordPress)
|
||||||
|
|
||||||
|
**Next.js oferece:**
|
||||||
|
- ✅ SSR out-of-the-box
|
||||||
|
- ✅ API routes (BFF pattern)
|
||||||
|
- ✅ Otimizações automáticas (image, fonts)
|
||||||
|
- ✅ Deploy simplificado (Vercel, Azure)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Por que manter WordPress?
|
||||||
|
|
||||||
|
**Alternativas consideradas:**
|
||||||
|
1. ❌ Migrar conteúdo para banco + CMS custom (tempo)
|
||||||
|
2. ❌ Strapi/Contentful (custos + learning curve)
|
||||||
|
3. ✅ **WordPress headless** (melhor trade-off)
|
||||||
|
|
||||||
|
**Vantagens:**
|
||||||
|
- Time de marketing já sabe usar
|
||||||
|
- Conteúdo aprovado já estava lá
|
||||||
|
- WordPress REST API é sólida
|
||||||
|
- Custo zero (já estava rodando)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Por que .NET 7 em vez de Node.js?
|
||||||
|
|
||||||
|
**Contexto:** Cliente tinha preferência por Microsoft stack.
|
||||||
|
|
||||||
|
**Benefícios adicionais:**
|
||||||
|
- Performance superior (vs Node em APIs)
|
||||||
|
- Type safety nativa (C#)
|
||||||
|
- Entity Framework (ORM maduro)
|
||||||
|
- Integração fácil com Azure (deploy futuro)
|
||||||
|
- Time do cliente tinha familiaridade
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Infraestrutura Atrasada? Prepare Alternativas
|
||||||
|
|
||||||
|
Não espere servidores/credenciais para começar:
|
||||||
|
- Docker local é seu amigo
|
||||||
|
- Mocks permitem progresso
|
||||||
|
- CI/CD pode ser preparado antes de haver onde deployar
|
||||||
|
|
||||||
|
**Lição:** Controle o que você pode controlar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Processos > Ferramentas
|
||||||
|
|
||||||
|
Mesmo sem Git corporativo, estabeleci:
|
||||||
|
- Branching strategy
|
||||||
|
- Code review
|
||||||
|
- Commit conventions
|
||||||
|
- Documentation standards
|
||||||
|
|
||||||
|
**Resultado:** Quando ferramentas chegaram, time já sabia usá-las.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Upskilling é Investimento, Não Custo
|
||||||
|
|
||||||
|
Pair programming e code reviews levaram tempo, mas:
|
||||||
|
- ✅ Time ficou autônomo mais rápido
|
||||||
|
- ✅ Qualidade de código aumentou
|
||||||
|
- ✅ Knowledge sharing natural
|
||||||
|
- ✅ Onboarding de novos devs simplificado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Resiliência Desde o Início
|
||||||
|
|
||||||
|
Implementar Polly (retry, circuit breaker) no início salvou em produção:
|
||||||
|
- Twilio teve instabilidade (resolvida automaticamente)
|
||||||
|
- Salesforce teve manutenção (queue funcionou)
|
||||||
|
- CRM API teve lentidão (cache mitigou)
|
||||||
|
|
||||||
|
**Lição:** Não deixe resiliência para "depois". Falhas vão acontecer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Comunicação Clara de Riscos
|
||||||
|
|
||||||
|
Reportei semanalmente:
|
||||||
|
- Bloqueios (infraestrutura, credenciais)
|
||||||
|
- Riscos (prazos, dependências)
|
||||||
|
- Soluções alternativas (mocks, workarounds)
|
||||||
|
|
||||||
|
**Resultado:** Stakeholders sabiam exatamente o status e não tiveram surpresas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenges & How They Were Overcome
|
||||||
|
|
||||||
|
| Desafio | Impacto | Solução | Resultado |
|
||||||
|
|---------|---------|---------|-----------|
|
||||||
|
| Sem Git | Bloqueio total | Setup local + GitLab Cloud | Time produtivo dia 1 |
|
||||||
|
| Sem servidores | Sem ambiente de dev | Docker Compose local | Dev/test local completo |
|
||||||
|
| Credenciais atrasadas | Integração bloqueada | Mock services | Progresso sem bloqueio |
|
||||||
|
| Time inexperiente | Código de baixa qualidade | Pair prog + Code review | Ramp-up em 4 semanas |
|
||||||
|
| Múltiplas integrações | Complexidade alta | Polly + patterns | Zero downtime prod |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps (Pós-MVP)
|
||||||
|
|
||||||
|
**Roadmap sugerido ao cliente:**
|
||||||
|
|
||||||
|
1. **Fase 2: Expansão de funcionalidades**
|
||||||
|
- Dashboard para médicos (histórico de pedidos)
|
||||||
|
- Notificações push (Firebase)
|
||||||
|
- Integração com e-commerce (compra direta)
|
||||||
|
|
||||||
|
2. **Fase 3: Otimizações**
|
||||||
|
- Cache distribuído (Redis)
|
||||||
|
- CDN para assets estáticos
|
||||||
|
- Analytics avançado (Amplitude)
|
||||||
|
|
||||||
|
3. **Fase 4: Escala**
|
||||||
|
- Kubernetes (AKS)
|
||||||
|
- Microserviços (quebrar monolito)
|
||||||
|
- Event-driven architecture (Azure Service Bus)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Resultado:** MVP entregue em produção apesar de começar literalmente do zero, estabelecendo fundações sólidas para transformação digital do cliente.
|
||||||
|
|
||||||
|
[Precisa construir um MVP em cenário desafiador? Entre em contato](#contact)
|
||||||
211
Content/Cases/pt/sap-integration-healthcare.md
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
---
|
||||||
|
title: "Sistema de Integração SAP Healthcare"
|
||||||
|
slug: "sap-integration-healthcare"
|
||||||
|
summary: "Integração bidirecional processando 100k+ transações/dia com 99.9% uptime"
|
||||||
|
client: "Confidencial - Multinacional Healthcare"
|
||||||
|
industry: "Healthcare"
|
||||||
|
timeline: "6 meses"
|
||||||
|
role: "Arquiteto de Integração"
|
||||||
|
image: ""
|
||||||
|
tags:
|
||||||
|
- SAP
|
||||||
|
- C#
|
||||||
|
- .NET
|
||||||
|
- Integrações
|
||||||
|
- Enterprise
|
||||||
|
- Healthcare
|
||||||
|
featured: true
|
||||||
|
order: 1
|
||||||
|
date: 2023-06-15
|
||||||
|
seo_title: "Case: Integração SAP Healthcare - 100k Transações/Dia"
|
||||||
|
seo_description: "Como arquitetamos sistema de integração SAP processando 100k+ transações diárias com 99.9% uptime para empresa healthcare."
|
||||||
|
seo_keywords: "integração SAP, C#, .NET, SAP Connector, enterprise integration, healthcare"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
**Cliente:** Multinacional Healthcare (confidencial)
|
||||||
|
**Porte:** 100.000+ funcionários
|
||||||
|
**Projeto:** Integração de benefícios
|
||||||
|
**Timeline:** 6 meses
|
||||||
|
**Meu Role:** Arquiteto de Integração
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Desafio
|
||||||
|
|
||||||
|
O cliente tinha sistema interno de gestão de benefícios que precisava sincronizar com SAP ECC para processar folha de pagamento.
|
||||||
|
|
||||||
|
### Dores principais:
|
||||||
|
- Processo manual sujeito a erros
|
||||||
|
- 3-5 dias de delay entre sistemas
|
||||||
|
- 100k funcionários esperando processamento
|
||||||
|
- Picos de carga (final de mês)
|
||||||
|
|
||||||
|
### Constraints:
|
||||||
|
- Budget limitado (sem SAP BTP)
|
||||||
|
- Time SAP interno pequeno (2 desenvolvedores)
|
||||||
|
- Prazo apertado (6 meses go-live)
|
||||||
|
- Sistema legado .NET Framework 4.5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solução
|
||||||
|
|
||||||
|
Arquitetura de integração bidirecional:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Sistema Interno] ←→ [Queue] ←→ [SAP Connector] ←→ [SAP ECC]
|
||||||
|
↓ ↓
|
||||||
|
[MongoDB Logs] [ABAP Z_BENEFITS]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Componentes:
|
||||||
|
- .NET Service com SAP Connector (NCo 3.0)
|
||||||
|
- ABAP transaction customizada (Z_BENEFITS)
|
||||||
|
- Queue system (RabbitMQ) para retry logic
|
||||||
|
- MongoDB para auditoria e troubleshooting
|
||||||
|
- Scheduler (Hangfire) para batch processing
|
||||||
|
|
||||||
|
### Fluxo:
|
||||||
|
1. Sistema gera mudanças (new hire, alterações)
|
||||||
|
2. Service processa batch (500 registros/vez)
|
||||||
|
3. SAP Connector chama Z_BENEFITS via RFC
|
||||||
|
4. SAP retorna status (sucesso/erro)
|
||||||
|
5. Retry automático se falha (max 3x)
|
||||||
|
6. Logs MongoDB para troubleshooting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resultado
|
||||||
|
|
||||||
|
### Métricas:
|
||||||
|
- **100k+** transações/dia processadas
|
||||||
|
- **99.9%** uptime
|
||||||
|
- Redução de **5 dias → 4 horas** (delay)
|
||||||
|
- **80%** redução tempo processamento
|
||||||
|
- **Zero** erros manuais (vs 2-3% antes)
|
||||||
|
|
||||||
|
### Benefícios:
|
||||||
|
- Funcionários recebem benefícios on-time
|
||||||
|
- Time RH economiza 40h/mês (trabalho manual)
|
||||||
|
- Auditoria completa (compliance)
|
||||||
|
- Escalável (crescimento 30% ano sem refactor)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
`C#` `.NET Framework 4.5` `SAP NCo 3.0` `RabbitMQ` `MongoDB` `Hangfire` `Docker` `SAP ECC` `ABAP` `RFC`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decisões & Motivação
|
||||||
|
|
||||||
|
### 💡 Decisão 1: SAP Connector vs SAP BTP
|
||||||
|
|
||||||
|
**Opções avaliadas:**
|
||||||
|
- SAP BTP (eventos, APIs modernas, cloud)
|
||||||
|
- SAP Connector (RFC direto, on-premise)
|
||||||
|
|
||||||
|
**Escolhemos:** SAP Connector
|
||||||
|
|
||||||
|
**Motivação:**
|
||||||
|
- Cliente tinha SAP ECC on-premise (não S/4)
|
||||||
|
- Budget não permitia licença BTP
|
||||||
|
- Time SAP confortável com ABAP/RFC
|
||||||
|
- Necessidades atendidas com RFC (não precisava event-driven real-time)
|
||||||
|
|
||||||
|
**Trade-off aceito:**
|
||||||
|
- Menos "moderno" que BTP, mas 100% funcional
|
||||||
|
- Custo R$ 0 adicional vs R$ 150k+/ano BTP
|
||||||
|
- Delivery 2 meses mais rápido (sem learning curve BTP)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💡 Decisão 2: Queue System vs Calls Diretos
|
||||||
|
|
||||||
|
**Opções avaliadas:**
|
||||||
|
- Chamadas síncronas diretas (mais simples)
|
||||||
|
- Queue com retry (mais complexo)
|
||||||
|
|
||||||
|
**Escolhemos:** Queue + Retry
|
||||||
|
|
||||||
|
**Motivação:**
|
||||||
|
- SAP ocasionalmente indisponível (manutenção)
|
||||||
|
- Picos de carga (final do mês = 200k reqs)
|
||||||
|
- Garantir zero perda de dados
|
||||||
|
- Resiliência > simplicidade (ambiente crítico)
|
||||||
|
|
||||||
|
**Implementação:**
|
||||||
|
- RabbitMQ com dead-letter queue
|
||||||
|
- Retry exponencial (1min, 5min, 15min)
|
||||||
|
- Alertas se 3 falhas consecutivas
|
||||||
|
|
||||||
|
**Resultado:**
|
||||||
|
- Zero perda dados em 2 anos produção
|
||||||
|
- Time RH não precisa "ficar de olho"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 💡 Decisão 3: ABAP Customizado vs Standard
|
||||||
|
|
||||||
|
**Opções avaliadas:**
|
||||||
|
- BAPIs standard SAP (zero código ABAP)
|
||||||
|
- Transaction customizada (Z_BENEFITS)
|
||||||
|
|
||||||
|
**Escolhemos:** Transaction customizada
|
||||||
|
|
||||||
|
**Motivação:**
|
||||||
|
- BAPIs standard não tinham validações específicas do negócio
|
||||||
|
- Cliente queria lógica centralizada no SAP (single source of truth)
|
||||||
|
- Permitiu validações complexas (elegibilidade, dependentes, limites)
|
||||||
|
|
||||||
|
**Trade-off:**
|
||||||
|
- Requer manutenção ABAP (time SAP interno)
|
||||||
|
- Mas: Cliente preferiu vs lógica dupla (risco dessincronia)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ Alternativas NÃO Escolhidas
|
||||||
|
|
||||||
|
**Webhook/Callback (Event-Driven):**
|
||||||
|
- Cliente não tinha infraestrutura expor APIs
|
||||||
|
- Sistema interno atrás de firewall
|
||||||
|
- Polling batch funciona bem para o caso
|
||||||
|
|
||||||
|
**Microserviços Kubernetes:**
|
||||||
|
- Overkill para integração única
|
||||||
|
- Time não tinha expertise K8s
|
||||||
|
- Docker simples suficiente
|
||||||
|
|
||||||
|
**Real-time Sync (<1min):**
|
||||||
|
- Negócio não precisa (batch diário ok)
|
||||||
|
- Custo infra aumentaria 3x
|
||||||
|
- 4h delay é aceitável para folha
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aprendizados
|
||||||
|
|
||||||
|
### ✅ O que funcionou muito bem:
|
||||||
|
- Envolver time SAP desde dia 1 (buy-in)
|
||||||
|
- MongoDB para logs (troubleshooting 10x mais rápido)
|
||||||
|
- Retry logic salvou inúmeras vezes
|
||||||
|
|
||||||
|
### 🔄 O que faria diferente:
|
||||||
|
- Adicionar health check endpoint mais cedo
|
||||||
|
- Dashboard de monitoramento desde início (adicionamos depois)
|
||||||
|
|
||||||
|
### 📚 Lições para próximos projetos:
|
||||||
|
- Cliente "budget limitado" ≠ "solução limitada" - criatividade resolve
|
||||||
|
- Documentar TODAS decisões arquiteturais (team turnover)
|
||||||
|
- Simplicidade vence complexidade quando ambas funcionam (KISS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Precisa de Algo Similar?
|
||||||
|
|
||||||
|
Integrações SAP complexas, sistemas legados, ou arquitetura de alta disponibilidade?
|
||||||
|
|
||||||
|
[Vamos conversar sobre seu desafio →](/#contact)
|
||||||
61
Controllers/CasesController.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using CarneiroTech.Services;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Controllers;
|
||||||
|
|
||||||
|
public class CasesController : Controller
|
||||||
|
{
|
||||||
|
private readonly ICaseService _caseService;
|
||||||
|
private readonly ILogger<CasesController> _logger;
|
||||||
|
|
||||||
|
public CasesController(ICaseService caseService, ILogger<CasesController> logger)
|
||||||
|
{
|
||||||
|
_caseService = caseService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: /cases
|
||||||
|
public async Task<IActionResult> Index(string? tag)
|
||||||
|
{
|
||||||
|
var cases = string.IsNullOrEmpty(tag)
|
||||||
|
? await _caseService.GetAllCasesAsync()
|
||||||
|
: await _caseService.GetCasesByTagAsync(tag);
|
||||||
|
|
||||||
|
var allTags = await _caseService.GetAllTagsAsync();
|
||||||
|
|
||||||
|
ViewData["Title"] = string.IsNullOrEmpty(tag)
|
||||||
|
? "Cases - Carneiro Tech"
|
||||||
|
: $"Cases: {tag} - Carneiro Tech";
|
||||||
|
ViewData["Description"] = string.IsNullOrEmpty(tag)
|
||||||
|
? "Explore our portfolio of technical consulting projects and solution design cases."
|
||||||
|
: $"Technical consulting cases related to {tag}.";
|
||||||
|
ViewData["SelectedTag"] = tag;
|
||||||
|
ViewBag.AllTags = allTags;
|
||||||
|
|
||||||
|
return View(cases);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET: /cases/{slug}
|
||||||
|
[Route("cases/{slug}")]
|
||||||
|
public async Task<IActionResult> Details(string slug)
|
||||||
|
{
|
||||||
|
var caseModel = await _caseService.GetCaseBySlugAsync(slug);
|
||||||
|
|
||||||
|
if (caseModel == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning($"Case not found: {slug}");
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewData["Title"] = !string.IsNullOrEmpty(caseModel.Metadata.SeoTitle)
|
||||||
|
? caseModel.Metadata.SeoTitle
|
||||||
|
: caseModel.Metadata.Title;
|
||||||
|
ViewData["Description"] = !string.IsNullOrEmpty(caseModel.Metadata.SeoDescription)
|
||||||
|
? caseModel.Metadata.SeoDescription
|
||||||
|
: caseModel.Metadata.Summary;
|
||||||
|
ViewData["Keywords"] = caseModel.Metadata.SeoKeywords;
|
||||||
|
ViewData["OgImage"] = caseModel.Metadata.Image;
|
||||||
|
|
||||||
|
return View(caseModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Controllers/HomeController.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using CarneiroTech.Models;
|
||||||
|
using CarneiroTech.Services;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Controllers;
|
||||||
|
|
||||||
|
public class HomeController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILogger<HomeController> _logger;
|
||||||
|
private readonly ICaseService _caseService;
|
||||||
|
|
||||||
|
public HomeController(ILogger<HomeController> logger, ICaseService caseService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_caseService = caseService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
var featuredCases = await _caseService.GetFeaturedCasesAsync();
|
||||||
|
|
||||||
|
ViewData["Title"] = "Carneiro Tech - Solution Design & Technical Consulting";
|
||||||
|
ViewData["Description"] = "20+ years connecting business and technology. Specialized in technical proposals, MVP definition, and due diligence.";
|
||||||
|
ViewData["Keywords"] = "solution design, technical consulting, SAP integration, enterprise architecture, MVP definition, due diligence";
|
||||||
|
|
||||||
|
return View(featuredCases);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IActionResult Privacy()
|
||||||
|
{
|
||||||
|
ViewData["Title"] = "Privacy Policy - Carneiro Tech";
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
|
public IActionResult Error()
|
||||||
|
{
|
||||||
|
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Route("sitemap.xml")]
|
||||||
|
public async Task<IActionResult> Sitemap()
|
||||||
|
{
|
||||||
|
var cases = await _caseService.GetAllCasesAsync();
|
||||||
|
|
||||||
|
var sitemap = new StringBuilder();
|
||||||
|
sitemap.AppendLine("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||||
|
sitemap.AppendLine("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">");
|
||||||
|
|
||||||
|
// Homepage
|
||||||
|
sitemap.AppendLine($@" <url>
|
||||||
|
<loc>https://carneirotech.com/</loc>
|
||||||
|
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>");
|
||||||
|
|
||||||
|
// Cases index
|
||||||
|
sitemap.AppendLine($@" <url>
|
||||||
|
<loc>https://carneirotech.com/cases</loc>
|
||||||
|
<lastmod>{DateTime.UtcNow:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>");
|
||||||
|
|
||||||
|
// Each case
|
||||||
|
foreach (var c in cases)
|
||||||
|
{
|
||||||
|
var lastMod = c.Metadata.Date != DateTime.MinValue ? c.Metadata.Date : DateTime.UtcNow;
|
||||||
|
sitemap.AppendLine($@" <url>
|
||||||
|
<loc>https://carneirotech.com/cases/{c.Metadata.Slug}</loc>
|
||||||
|
<lastmod>{lastMod:yyyy-MM-dd}</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>");
|
||||||
|
}
|
||||||
|
|
||||||
|
sitemap.AppendLine("</urlset>");
|
||||||
|
|
||||||
|
return Content(sitemap.ToString(), "application/xml");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult Contact(ContactFormModel model)
|
||||||
|
{
|
||||||
|
if (!ModelState.IsValid)
|
||||||
|
{
|
||||||
|
TempData["Error"] = "Por favor, preencha todos os campos corretamente.";
|
||||||
|
return RedirectToAction("Index", new { fragment = "contact" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement email sending logic here (SendGrid, SMTP, etc.)
|
||||||
|
_logger.LogInformation($"Contact form submitted: {model.Name} - {model.Email}");
|
||||||
|
|
||||||
|
TempData["Success"] = "Mensagem enviada com sucesso! Retornaremos em breve.";
|
||||||
|
return RedirectToAction("Index", new { fragment = "contact" });
|
||||||
|
}
|
||||||
|
}
|
||||||
28
Controllers/LanguageController.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using CarneiroTech.Services;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Controllers
|
||||||
|
{
|
||||||
|
public class LanguageController : Controller
|
||||||
|
{
|
||||||
|
private readonly ILanguageService _languageService;
|
||||||
|
|
||||||
|
public LanguageController(ILanguageService languageService)
|
||||||
|
{
|
||||||
|
_languageService = languageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult SetLanguage(string language, string returnUrl = "/")
|
||||||
|
{
|
||||||
|
_languageService.SetLanguage(HttpContext, language);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||||
|
{
|
||||||
|
return Redirect(returnUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("Index", "Home");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
363
DEPLOY_README.md
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
# CarneiroTech - Deployment Guide
|
||||||
|
|
||||||
|
## 🚀 Deploy para Produção (OCI)
|
||||||
|
|
||||||
|
O site CarneiroTech está hospedado no servidor OCI (Oracle Cloud Infrastructure) com IP final **218**.
|
||||||
|
|
||||||
|
### Arquitetura
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet
|
||||||
|
↓
|
||||||
|
Nginx (Port 80/443)
|
||||||
|
↓
|
||||||
|
Docker Container (Port 5008)
|
||||||
|
↓
|
||||||
|
ASP.NET Core MVC App
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Informações do Servidor
|
||||||
|
|
||||||
|
- **Servidor**: `ubuntu@129.146.116.218`
|
||||||
|
- **Diretório**: `/home/ubuntu/apps/carneirotech`
|
||||||
|
- **Container**: `carneirotech-web`
|
||||||
|
- **Porta Interna**: `5008`
|
||||||
|
- **Domínio**: `carneirotech.com`
|
||||||
|
- **SSL**: Let's Encrypt (auto-renovação configurada)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Como Fazer Deploy
|
||||||
|
|
||||||
|
### Opção 1: Deploy Automático (Recomendado)
|
||||||
|
|
||||||
|
Execute o script de deploy da sua máquina local:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/c/Users/ricar/Nextcloud/CarneiroTech/Site/aspnet/CarneiroTech
|
||||||
|
./deploy-to-oci.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script irá:
|
||||||
|
1. ✅ Criar pacote compactado excluindo bin/obj
|
||||||
|
2. ✅ Transferir para servidor via SCP
|
||||||
|
3. ✅ Extrair arquivos
|
||||||
|
4. ✅ Fazer backup da imagem anterior
|
||||||
|
5. ✅ Fazer build da nova imagem
|
||||||
|
6. ✅ Parar container antigo
|
||||||
|
7. ✅ Subir container novo
|
||||||
|
8. ✅ Verificar health check
|
||||||
|
9. ✅ Limpar imagens antigas (mantém últimas 3)
|
||||||
|
|
||||||
|
**Rollback automático:** Se o health check falhar, o script restaura automaticamente a versão anterior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Opção 2: Deploy Manual
|
||||||
|
|
||||||
|
1. Conectar no servidor:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218
|
||||||
|
cd ~/apps/carneirotech
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Transferir arquivos alterados (da sua máquina):
|
||||||
|
```bash
|
||||||
|
scp arquivo.cs ubuntu@129.146.116.218:~/apps/carneirotech/Controllers/
|
||||||
|
```
|
||||||
|
|
||||||
|
3. No servidor, fazer rebuild e restart:
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Comandos Úteis
|
||||||
|
|
||||||
|
### Ver logs do container
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker logs carneirotech-web --tail=50 -f"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar status
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker ps --filter name=carneirotech"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entrar no container
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker exec -it carneirotech-web bash"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reiniciar container
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "cd ~/apps/carneirotech && docker compose restart"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver logs do Nginx
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo tail -f /var/log/nginx/access.log"
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo tail -f /var/log/nginx/error.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testar Nginx config
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo nginx -t"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recarregar Nginx
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo systemctl reload nginx"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Estrutura de Arquivos no Servidor
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/ubuntu/apps/carneirotech/
|
||||||
|
├── Content/ # Markdown files dos cases
|
||||||
|
├── Controllers/ # MVC Controllers
|
||||||
|
├── Models/ # Models
|
||||||
|
├── Services/ # Services (CaseService)
|
||||||
|
├── Views/ # Razor Views
|
||||||
|
├── wwwroot/ # Static files (CSS, JS, images)
|
||||||
|
├── Dockerfile # Docker build instructions (ARM64)
|
||||||
|
├── docker-compose.yml # Docker Compose config
|
||||||
|
├── deploy.sh # Deploy script (no servidor)
|
||||||
|
└── CarneiroTech.csproj # Project file
|
||||||
|
|
||||||
|
/etc/nginx/sites-available/
|
||||||
|
└── carneirotech.com.conf # Nginx proxy config
|
||||||
|
|
||||||
|
/etc/letsencrypt/live/carneirotech.com/
|
||||||
|
├── fullchain.pem # SSL certificate
|
||||||
|
└── privkey.pem # SSL private key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 SSL / HTTPS
|
||||||
|
|
||||||
|
O certificado SSL é gerenciado automaticamente pelo **Let's Encrypt** via Certbot.
|
||||||
|
|
||||||
|
### Renovação Automática
|
||||||
|
|
||||||
|
O Certbot está configurado para renovar automaticamente. Verifique o status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo certbot certificates"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renovar Manualmente (se necessário)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo certbot renew"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adicionar www.carneirotech.com
|
||||||
|
|
||||||
|
Quando você adicionar o registro DNS para `www.carneirotech.com`:
|
||||||
|
|
||||||
|
1. Adicionar ao Namecheap (A record apontando para 129.146.116.218)
|
||||||
|
2. Atualizar certificado SSL:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo certbot certonly --nginx -d carneirotech.com -d www.carneirotech.com --expand"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Atualizar nginx config:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo nano /etc/nginx/sites-available/carneirotech.com.conf"
|
||||||
|
```
|
||||||
|
Adicionar `www.carneirotech.com` no `server_name` de ambos os blocos server.
|
||||||
|
|
||||||
|
4. Recarregar nginx:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo nginx -t && sudo systemctl reload nginx"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧹 Manutenção
|
||||||
|
|
||||||
|
### Limpar imagens Docker antigas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker image prune -a"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limpar containers parados
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker container prune"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver uso de disco
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "df -h"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ver uso de Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.218 "docker system df"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Site não carrega (502 Bad Gateway)
|
||||||
|
|
||||||
|
1. Verificar se container está rodando:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker ps --filter name=carneirotech"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ver logs do container:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker logs carneirotech-web --tail=100"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Reiniciar container:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "cd ~/apps/carneirotech && docker compose restart"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container não sobe (unhealthy)
|
||||||
|
|
||||||
|
1. Ver logs detalhados:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker logs carneirotech-web"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Verificar health check manualmente:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "curl http://localhost:5008/"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Entrar no container para debug:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker exec -it carneirotech-web bash"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build falha
|
||||||
|
|
||||||
|
1. Verificar se há espaço em disco:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "df -h"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Limpar cache do Docker:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker builder prune -a"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Fazer build com logs detalhados:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "cd ~/apps/carneirotech && docker compose build --no-cache --progress=plain"
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL expirado
|
||||||
|
|
||||||
|
Renovar manualmente:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "sudo certbot renew --force-renewal"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Monitoramento
|
||||||
|
|
||||||
|
### Health Check Endpoint
|
||||||
|
|
||||||
|
O container tem health check automático configurado:
|
||||||
|
- **URL**: `http://localhost:5008/`
|
||||||
|
- **Intervalo**: 30s
|
||||||
|
- **Timeout**: 3s
|
||||||
|
- **Start Period**: 40s
|
||||||
|
|
||||||
|
### Ver status de saúde
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker inspect carneirotech-web | grep -A 10 Health"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Workflow de Desenvolvimento
|
||||||
|
|
||||||
|
1. **Desenvolver localmente** (Windows)
|
||||||
|
```bash
|
||||||
|
cd C:\Users\ricar\Nextcloud\CarneiroTech\Site\aspnet\CarneiroTech
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Testar mudanças** (http://localhost:5000)
|
||||||
|
|
||||||
|
3. **Fazer deploy para produção**
|
||||||
|
```bash
|
||||||
|
./deploy-to-oci.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verificar em produção** (https://carneirotech.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notas Importantes
|
||||||
|
|
||||||
|
- ⚠️ O servidor usa **ARM64** (Ampere). O Dockerfile está otimizado para isso.
|
||||||
|
- ⚠️ Não use `docker-compose` sem `deploy.sh` - pode causar downtime sem rollback.
|
||||||
|
- ⚠️ Sempre teste localmente antes de fazer deploy.
|
||||||
|
- ⚠️ O script de deploy faz backup automático - use sem medo!
|
||||||
|
- ⚠️ Arquivos Markdown (Content/Cases/*.md) são incluídos no build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist de Deploy
|
||||||
|
|
||||||
|
Antes de fazer deploy, verifique:
|
||||||
|
|
||||||
|
- [ ] Código compilou localmente sem erros
|
||||||
|
- [ ] Testou localmente (dotnet run)
|
||||||
|
- [ ] Commitou mudanças no Git (quando configurar)
|
||||||
|
- [ ] Executou `./deploy-to-oci.sh`
|
||||||
|
- [ ] Verificou logs: `docker logs carneirotech-web`
|
||||||
|
- [ ] Testou site: `https://carneirotech.com`
|
||||||
|
- [ ] Verificou que cases Markdown aparecem corretamente
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Suporte
|
||||||
|
|
||||||
|
Em caso de problemas críticos:
|
||||||
|
|
||||||
|
1. Ver logs em tempo real:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218 "docker logs carneirotech-web -f"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Rollback para versão anterior (automático no deploy.sh)
|
||||||
|
|
||||||
|
3. Se precisar voltar manualmente:
|
||||||
|
```bash
|
||||||
|
ssh ubuntu@129.146.116.218
|
||||||
|
cd ~/backups/carneirotech
|
||||||
|
# Ver backups disponíveis
|
||||||
|
docker images | grep carneirotech-web
|
||||||
|
# Usar backup específico
|
||||||
|
docker tag <BACKUP_IMAGE_ID> carneirotech-carneirotech-web
|
||||||
|
cd ~/apps/carneirotech
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Deployment criado em:** 21 de Dezembro de 2024
|
||||||
|
**Última atualização:** 21 de Dezembro de 2024
|
||||||
|
**Servidor:** OCI (Oracle Cloud) - ARM64 (Ampere)
|
||||||
|
**Status:** ✅ Produção
|
||||||
44
Dockerfile
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Dockerfile for ASP.NET Core MVC on ARM64 (Ampere/OCI)
|
||||||
|
|
||||||
|
# Build stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy-arm64v8 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Copy csproj and restore dependencies
|
||||||
|
COPY ["CarneiroTech.csproj", "./"]
|
||||||
|
RUN dotnet restore "CarneiroTech.csproj" -a arm64
|
||||||
|
|
||||||
|
# Copy everything else and build
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet build "CarneiroTech.csproj" -c Release -o /app/build -a arm64
|
||||||
|
|
||||||
|
# Publish stage
|
||||||
|
FROM build AS publish
|
||||||
|
RUN dotnet publish "CarneiroTech.csproj" -c Release -o /app/publish -a arm64 /p:UseAppHost=false
|
||||||
|
|
||||||
|
# Runtime stage
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-arm64v8 AS final
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install curl for health checks
|
||||||
|
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy published app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
|
||||||
|
# Copy Content folder (markdown files) - already included in publish
|
||||||
|
# COPY Content ./Content
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 5008
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:5008/ || exit 1
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5008
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
|
||||||
|
# Run app
|
||||||
|
ENTRYPOINT ["dotnet", "CarneiroTech.dll"]
|
||||||
308
IMPLEMENTATION_SUMMARY.md
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
# Carneiro Tech - Implementation Summary
|
||||||
|
|
||||||
|
## What Has Been Built
|
||||||
|
|
||||||
|
A complete, production-ready ASP.NET MVC Core website for Carneiro Tech with the following features:
|
||||||
|
|
||||||
|
### ✅ Core Features Implemented
|
||||||
|
|
||||||
|
1. **Modern, Responsive Design**
|
||||||
|
- Agency Bootstrap template fully integrated
|
||||||
|
- Custom CSS with logo colors (#ffc800 yellow/gold)
|
||||||
|
- Mobile-first responsive layout
|
||||||
|
- Professional consulting appearance
|
||||||
|
|
||||||
|
2. **Markdown-Based Portfolio System**
|
||||||
|
- Easy case management with .md files
|
||||||
|
- YAML front matter for metadata
|
||||||
|
- Automatic HTML conversion
|
||||||
|
- In-memory caching (60 min)
|
||||||
|
- Tag-based filtering
|
||||||
|
|
||||||
|
3. **SEO Optimized**
|
||||||
|
- Dynamic meta tags (title, description, keywords)
|
||||||
|
- Open Graph tags for social sharing
|
||||||
|
- Twitter Card support
|
||||||
|
- JSON-LD structured data (Organization schema)
|
||||||
|
- XML sitemap at `/sitemap.xml`
|
||||||
|
- robots.txt configured
|
||||||
|
|
||||||
|
4. **Portfolio/Cases Section**
|
||||||
|
- Homepage with featured cases (up to 6)
|
||||||
|
- Full cases listing page at `/cases`
|
||||||
|
- Individual case detail pages at `/cases/{slug}`
|
||||||
|
- Tag filtering functionality
|
||||||
|
- Custom hover effects with overlay text
|
||||||
|
- Gradient backgrounds for cases without images
|
||||||
|
|
||||||
|
5. **Docker Ready**
|
||||||
|
- Multi-stage Dockerfile
|
||||||
|
- docker-compose.yml for easy deployment
|
||||||
|
- .dockerignore for optimized builds
|
||||||
|
- Volume mapping for Content folder
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
aspnet/CarneiroTech/
|
||||||
|
├── Controllers/
|
||||||
|
│ ├── HomeController.cs # Homepage, sitemap, contact
|
||||||
|
│ └── CasesController.cs # Cases list & details
|
||||||
|
├── Models/
|
||||||
|
│ ├── CaseModel.cs # Case data model
|
||||||
|
│ ├── CaseMetadata.cs # YAML front matter model
|
||||||
|
│ ├── ContactFormModel.cs # Contact form validation
|
||||||
|
│ └── SitemapItem.cs # Sitemap data model
|
||||||
|
├── Services/
|
||||||
|
│ ├── IMarkdownService.cs # Markdown parsing interface
|
||||||
|
│ ├── MarkdownService.cs # Markdown implementation
|
||||||
|
│ ├── ICaseService.cs # Case management interface
|
||||||
|
│ └── CaseService.cs # Case management implementation
|
||||||
|
├── Views/
|
||||||
|
│ ├── Home/
|
||||||
|
│ │ └── Index.cshtml # Homepage with all sections
|
||||||
|
│ ├── Cases/
|
||||||
|
│ │ ├── Index.cshtml # Cases listing with filters
|
||||||
|
│ │ └── Details.cshtml # Individual case page
|
||||||
|
│ └── Shared/
|
||||||
|
│ └── _Layout.cshtml # Main layout with SEO
|
||||||
|
├── Content/
|
||||||
|
│ └── Cases/
|
||||||
|
│ ├── sap-integration-healthcare.md # Case 1
|
||||||
|
│ ├── legacy-modernization.md # Case 2
|
||||||
|
│ └── mvp-definition.md # Case 3
|
||||||
|
├── wwwroot/
|
||||||
|
│ ├── css/
|
||||||
|
│ │ ├── styles.css # Bootstrap Agency theme
|
||||||
|
│ │ └── custom.css # Custom Carneiro Tech styles
|
||||||
|
│ ├── js/
|
||||||
|
│ │ └── scripts.js # Bootstrap theme scripts
|
||||||
|
│ ├── img/
|
||||||
|
│ │ └── logo.svg # Carneiro Tech logo
|
||||||
|
│ └── robots.txt
|
||||||
|
├── Dockerfile
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── .dockerignore
|
||||||
|
├── README.md
|
||||||
|
└── IMPLEMENTATION_SUMMARY.md (this file)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Homepage Sections
|
||||||
|
|
||||||
|
The homepage includes all major sections:
|
||||||
|
|
||||||
|
1. **Hero/Masthead**
|
||||||
|
- "Conectando Negócio e Tecnologia" tagline
|
||||||
|
- Call-to-action button to services
|
||||||
|
|
||||||
|
2. **Services Section** (#services)
|
||||||
|
- Solution Design
|
||||||
|
- Technical Consulting
|
||||||
|
- Technical Proposals
|
||||||
|
|
||||||
|
3. **Portfolio/Cases** (#portfolio)
|
||||||
|
- Featured cases (up to 6)
|
||||||
|
- Link to full cases page
|
||||||
|
|
||||||
|
4. **About Section** (#about)
|
||||||
|
- Timeline with 4 milestones
|
||||||
|
- Professional journey
|
||||||
|
- Icons for each phase
|
||||||
|
|
||||||
|
5. **Contact Form** (#contact)
|
||||||
|
- Name, Email, Phone, Message fields
|
||||||
|
- Form validation
|
||||||
|
- Success/Error messages
|
||||||
|
- Note: Email sending needs to be implemented (TODO comment in code)
|
||||||
|
|
||||||
|
## Three Example Cases Created
|
||||||
|
|
||||||
|
### 1. SAP Integration Healthcare
|
||||||
|
- **Slug:** `sap-integration-healthcare`
|
||||||
|
- **Topic:** Enterprise integration with SAP ECC
|
||||||
|
- **Tags:** SAP, C#, .NET, Integrações, Enterprise, Healthcare
|
||||||
|
- **Highlights:** 100k+ transactions/day, 99.9% uptime
|
||||||
|
- **Featured:** Yes
|
||||||
|
|
||||||
|
### 2. Legacy Modernization
|
||||||
|
- **Slug:** `legacy-modernization`
|
||||||
|
- **Topic:** Migration from monolith to microservices
|
||||||
|
- **Tags:** .NET, Azure, Microserviços, Cloud, Modernização, Arquitetura
|
||||||
|
- **Highlights:** Strangler pattern, AKS, zero downtime
|
||||||
|
- **Featured:** Yes
|
||||||
|
|
||||||
|
### 3. MVP Definition
|
||||||
|
- **Slug:** `mvp-definition`
|
||||||
|
- **Topic:** Startup consulting for MVP validation
|
||||||
|
- **Tags:** MVP, Product Design, Technical Consulting, Startup, EdTech, Strategy
|
||||||
|
- **Highlights:** Reduced scope 50→8 features, validated with 1000+ users
|
||||||
|
- **Featured:** Yes
|
||||||
|
|
||||||
|
Each case includes:
|
||||||
|
- Overview section
|
||||||
|
- Challenge description
|
||||||
|
- Solution architecture
|
||||||
|
- Results/metrics
|
||||||
|
- Tech stack
|
||||||
|
- Decision-making process (why certain choices were made)
|
||||||
|
- Lessons learned
|
||||||
|
|
||||||
|
## Key Features & Customizations
|
||||||
|
|
||||||
|
### Portfolio Hover Effect
|
||||||
|
- Yellow overlay (#ffc800) on hover
|
||||||
|
- Text summary appears on hover
|
||||||
|
- Smooth transitions
|
||||||
|
- Works on all devices
|
||||||
|
|
||||||
|
### Color Scheme
|
||||||
|
- Primary: #ffc800 (yellow/gold from logo)
|
||||||
|
- Dark: #212529
|
||||||
|
- Gradients for backgrounds without images
|
||||||
|
- Professional, modern feel
|
||||||
|
|
||||||
|
### SEO Features
|
||||||
|
All pages include:
|
||||||
|
- Title (max 60 chars recommended)
|
||||||
|
- Description (max 160 chars)
|
||||||
|
- Keywords
|
||||||
|
- Canonical URLs
|
||||||
|
- Open Graph tags
|
||||||
|
- Twitter Cards
|
||||||
|
- Structured data (JSON-LD)
|
||||||
|
|
||||||
|
### Performance Optimizations
|
||||||
|
- In-memory caching (cases cached for 60 min)
|
||||||
|
- Static file serving
|
||||||
|
- Minified CSS/JS from CDN
|
||||||
|
- Lazy loading compatible
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd aspnet/CarneiroTech
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
Open: `http://localhost:5000`
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd aspnet/CarneiroTech
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Open: `http://localhost:8080`
|
||||||
|
|
||||||
|
## Next Steps / TODOs
|
||||||
|
|
||||||
|
### Essential for Production:
|
||||||
|
|
||||||
|
1. **Replace placeholder logo colors**
|
||||||
|
- Extract actual colors from `/logo/LogoNovo.svg`
|
||||||
|
- Update custom.css color variables
|
||||||
|
|
||||||
|
2. **Update LinkedIn URL**
|
||||||
|
- In `_Layout.cshtml` footer
|
||||||
|
- Currently: `https://linkedin.com/in/ricardo-carneiro`
|
||||||
|
|
||||||
|
3. **Implement Contact Form Email**
|
||||||
|
- Add SendGrid or SMTP configuration
|
||||||
|
- Update `HomeController.Contact` method
|
||||||
|
- Add email credentials to appsettings.json
|
||||||
|
|
||||||
|
4. **Add Real Portfolio Images**
|
||||||
|
- Currently cases use gradient backgrounds
|
||||||
|
- Add images to `/wwwroot/img/cases/`
|
||||||
|
- Update markdown front matter `image:` field
|
||||||
|
|
||||||
|
5. **Update Domain URLs**
|
||||||
|
- Replace `carneirotech.com` with your actual domain
|
||||||
|
- Files to update:
|
||||||
|
- `_Layout.cshtml` (canonical, OG tags)
|
||||||
|
- `HomeController.cs` (sitemap)
|
||||||
|
|
||||||
|
### Nice to Have:
|
||||||
|
|
||||||
|
1. **Analytics**
|
||||||
|
- Add Google Analytics 4
|
||||||
|
- Add tracking code to `_Layout.cshtml`
|
||||||
|
|
||||||
|
2. **Dark Mode**
|
||||||
|
- Implement toggle switch
|
||||||
|
- CSS variables for theming
|
||||||
|
|
||||||
|
3. **RSS Feed**
|
||||||
|
- Create `/feed.xml` endpoint
|
||||||
|
- List all cases
|
||||||
|
|
||||||
|
4. **Search Functionality**
|
||||||
|
- Add search bar
|
||||||
|
- Filter cases by text
|
||||||
|
|
||||||
|
5. **Admin Panel**
|
||||||
|
- CRUD for cases (instead of editing .md files)
|
||||||
|
- Cache invalidation button
|
||||||
|
|
||||||
|
6. **Performance**
|
||||||
|
- Image optimization (WebP format)
|
||||||
|
- Lazy loading for images
|
||||||
|
- CDN for static assets
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [x] Build succeeds without errors
|
||||||
|
- [x] All routes defined
|
||||||
|
- [x] Markdown parsing works
|
||||||
|
- [x] SEO meta tags present
|
||||||
|
- [x] Sitemap generates
|
||||||
|
- [ ] Run locally and verify homepage
|
||||||
|
- [ ] Verify cases listing page
|
||||||
|
- [ ] Verify individual case pages
|
||||||
|
- [ ] Test tag filtering
|
||||||
|
- [ ] Test contact form validation
|
||||||
|
- [ ] Test responsive design (mobile/tablet)
|
||||||
|
- [ ] Test in different browsers
|
||||||
|
- [ ] Validate HTML
|
||||||
|
- [ ] Test Docker build
|
||||||
|
- [ ] Test Docker run
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Update all placeholder URLs
|
||||||
|
- [ ] Add real logo and images
|
||||||
|
- [ ] Configure email sending
|
||||||
|
- [ ] Set up SSL certificate
|
||||||
|
- [ ] Configure production environment variables
|
||||||
|
- [ ] Test in staging environment
|
||||||
|
- [ ] Set up monitoring/logging
|
||||||
|
- [ ] Configure backup for Content folder
|
||||||
|
- [ ] Set up CI/CD pipeline (optional)
|
||||||
|
- [ ] Update DNS records
|
||||||
|
- [ ] Test production deployment
|
||||||
|
|
||||||
|
## Support & Documentation
|
||||||
|
|
||||||
|
- **README.md** - Complete setup and usage guide
|
||||||
|
- **This file** - Implementation overview
|
||||||
|
- **Code comments** - Inline documentation
|
||||||
|
- **Markdown examples** - 3 complete case studies
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
- **ASP.NET Core 8** - Web framework
|
||||||
|
- **C# 12** - Programming language
|
||||||
|
- **Markdig 0.44.0** - Markdown parsing
|
||||||
|
- **YamlDotNet 16.3.0** - YAML parsing
|
||||||
|
- **Bootstrap 5** - CSS framework
|
||||||
|
- **Font Awesome 6** - Icons
|
||||||
|
- **Docker** - Containerization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built on:** 2025-12-19
|
||||||
|
**Framework:** ASP.NET MVC Core 8
|
||||||
|
**Status:** Production Ready (pending customizations listed above)
|
||||||
215
MUDANCAS_TEMA_VERMELHO.md
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
# Mudanças Implementadas - Tema Vermelho
|
||||||
|
|
||||||
|
## ✅ Alterações Realizadas
|
||||||
|
|
||||||
|
### 1. **Esquema de Cores - Do Amarelo para Vermelho**
|
||||||
|
|
||||||
|
Todas as cores amarelas foram substituídas por tons de vermelho inspirados no logo:
|
||||||
|
|
||||||
|
**Paleta de Cores:**
|
||||||
|
- **Vermelho Principal:** `#C42127` (vermelho vibrante do logo)
|
||||||
|
- **Vermelho Escuro:** `#8B1E23` (tom mais escuro para hover/sombras)
|
||||||
|
- **Vermelho Accent:** `#E63946` (vermelho brilhante para destaques)
|
||||||
|
- **Fundo Claro:** `#FAF7F5` (bege/creme quente)
|
||||||
|
- **Creme:** `#FFF8F0` (creme claro para navbar)
|
||||||
|
|
||||||
|
### 2. **Menu/Navbar com Fundo Claro**
|
||||||
|
|
||||||
|
**Antes:** Navbar escuro (dark)
|
||||||
|
**Depois:** Navbar com fundo bege claro (`#FAF7F5`)
|
||||||
|
|
||||||
|
**Mudanças:**
|
||||||
|
- Fundo claro e confortável
|
||||||
|
- Links do menu em cinza escuro (`#2c3e50`)
|
||||||
|
- Hover em vermelho (`#C42127`)
|
||||||
|
- Sombra sutil para destaque
|
||||||
|
- Menu mobile com fundo creme
|
||||||
|
- Ícone do menu (hamburguer) em vermelho
|
||||||
|
|
||||||
|
### 3. **Logo Otimizado**
|
||||||
|
|
||||||
|
**Antes:** `logo.svg` (745KB, muito pesado)
|
||||||
|
**Depois:** `logo-optimized.svg` (<2KB, otimizado)
|
||||||
|
|
||||||
|
**Características do novo logo:**
|
||||||
|
- **Fundo transparente** ✅
|
||||||
|
- Design de espiral/concha inspirado no logo original
|
||||||
|
- Gradiente vermelho
|
||||||
|
- Tamanho reduzido 370x
|
||||||
|
- Altura aumentada de 40px para 45px (melhor visibilidade)
|
||||||
|
|
||||||
|
**Arquivo:** `/wwwroot/img/logo-optimized.svg`
|
||||||
|
|
||||||
|
### 4. **Efeito Hover no Portfolio - Corrigido**
|
||||||
|
|
||||||
|
**Problema:** Quadrado amarelo pequeno e sem sentido no hover
|
||||||
|
|
||||||
|
**Solução:**
|
||||||
|
- Overlay vermelho cobrindo **toda a imagem** do portfolio
|
||||||
|
- Texto do resumo aparece em branco sobre fundo vermelho
|
||||||
|
- Transição suave de opacidade
|
||||||
|
- Card levanta levemente no hover (translateY)
|
||||||
|
- Sombra vermelha ao redor do card
|
||||||
|
- Zoom sutil na imagem (scale 1.05)
|
||||||
|
|
||||||
|
**Efeito visual:**
|
||||||
|
1. Ao passar o mouse sobre um case
|
||||||
|
2. Overlay vermelho aparece cobrindo toda a área
|
||||||
|
3. Texto do resumo fica visível em branco
|
||||||
|
4. Card levanta 5px com sombra vermelha
|
||||||
|
5. Imagem dá zoom leve
|
||||||
|
|
||||||
|
### 5. **Elementos Atualizados com Vermelho**
|
||||||
|
|
||||||
|
#### Botões:
|
||||||
|
- `btn-primary`: Fundo vermelho, texto branco
|
||||||
|
- Hover: Tom mais escuro + levanta + sombra
|
||||||
|
- `btn-outline-primary`: Borda vermelha, preenche vermelho no hover
|
||||||
|
|
||||||
|
#### Badges/Tags:
|
||||||
|
- Fundo vermelho com texto branco
|
||||||
|
- Tags clicáveis nos cases
|
||||||
|
|
||||||
|
#### Ícones de Serviços:
|
||||||
|
- Círculos em vermelho (antes amarelo)
|
||||||
|
- Ícones brancos sobre vermelho
|
||||||
|
|
||||||
|
#### Timeline:
|
||||||
|
- Círculos da timeline em vermelho
|
||||||
|
- Ícones brancos
|
||||||
|
|
||||||
|
#### Seção de Contato:
|
||||||
|
- Fundo gradiente vermelho (C42127 → 8B1E23)
|
||||||
|
- Inputs com foco em vermelho
|
||||||
|
|
||||||
|
#### Detalhes do Case:
|
||||||
|
- Bordas de heading: vermelho
|
||||||
|
- Bordas de code blocks: vermelho
|
||||||
|
- Bordas de blockquotes: vermelho
|
||||||
|
- Links: vermelho
|
||||||
|
|
||||||
|
#### Masthead/Hero:
|
||||||
|
- Gradiente vermelho de fundo
|
||||||
|
|
||||||
|
#### Gradientes de Cases sem Imagem:
|
||||||
|
6 variações de gradientes vermelhos
|
||||||
|
|
||||||
|
### 6. **Arquivos Modificados**
|
||||||
|
|
||||||
|
```
|
||||||
|
✏️ /wwwroot/css/custom.css (reescrito completamente)
|
||||||
|
✏️ /Views/Shared/_Layout.cshtml (navbar light + logo otimizado)
|
||||||
|
✏️ /Views/Home/Index.cshtml (gradientes vermelhos)
|
||||||
|
✏️ /Views/Cases/Index.cshtml (gradientes vermelhos)
|
||||||
|
✏️ /Views/Cases/Details.cshtml (bordas vermelhas)
|
||||||
|
➕ /wwwroot/img/logo-optimized.svg (novo logo)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Comparação Visual
|
||||||
|
|
||||||
|
### Antes (Amarelo):
|
||||||
|
- 🟡 Botões amarelos (#ffc800)
|
||||||
|
- 🟡 Hover amarelo nos cases
|
||||||
|
- 🟡 Timeline amarela
|
||||||
|
- ⚫ Navbar escuro
|
||||||
|
- 📦 Logo 745KB
|
||||||
|
|
||||||
|
### Depois (Vermelho):
|
||||||
|
- 🔴 Botões vermelhos (#C42127)
|
||||||
|
- 🔴 Hover vermelho cobrindo todo o case
|
||||||
|
- 🔴 Timeline vermelha
|
||||||
|
- ⚪ Navbar bege claro
|
||||||
|
- ✨ Logo 2KB transparente
|
||||||
|
|
||||||
|
## 🚀 Como Testar
|
||||||
|
|
||||||
|
### Se o app já está rodando:
|
||||||
|
1. Pare a aplicação (Ctrl+C no terminal)
|
||||||
|
2. Rode novamente: `dotnet run`
|
||||||
|
3. Abra: `http://localhost:5000`
|
||||||
|
|
||||||
|
### Se não está rodando:
|
||||||
|
```bash
|
||||||
|
cd aspnet/CarneiroTech
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verificar mudanças:
|
||||||
|
1. **Homepage** - Veja os ícones vermelhos, botão vermelho
|
||||||
|
2. **Navbar** - Fundo claro, logo otimizado
|
||||||
|
3. **Cases** - Passe o mouse sobre um case → overlay vermelho
|
||||||
|
4. **Detalhes do Case** - Bordas vermelhas nos headings
|
||||||
|
5. **Contato** - Fundo vermelho na seção
|
||||||
|
|
||||||
|
## 📝 Notas Técnicas
|
||||||
|
|
||||||
|
### CSS Variables (custom.css):
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--primary-red: #C42127;
|
||||||
|
--dark-red: #8B1E23;
|
||||||
|
--accent-red: #E63946;
|
||||||
|
--light-bg: #FAF7F5;
|
||||||
|
--cream: #FFF8F0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Todas as cores agora usam essas variáveis, facilitando ajustes futuros.
|
||||||
|
|
||||||
|
### Navbar Classes:
|
||||||
|
- Mudado de `navbar-dark` para `navbar-light`
|
||||||
|
- Adicionado estilo customizado para fundo claro
|
||||||
|
- Links com cor escura e hover vermelho
|
||||||
|
|
||||||
|
### Portfolio Hover:
|
||||||
|
```css
|
||||||
|
.portfolio-hover {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(196, 33, 39, 0.95);
|
||||||
|
/* Cobre todo o card */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✨ Próximos Passos (Opcional)
|
||||||
|
|
||||||
|
Se quiser fazer ajustes finos:
|
||||||
|
|
||||||
|
1. **Ajustar tom de vermelho:**
|
||||||
|
- Edite as variáveis CSS no topo de `custom.css`
|
||||||
|
- Todos os elementos atualizam automaticamente
|
||||||
|
|
||||||
|
2. **Mudar fundo do menu:**
|
||||||
|
- Edite `--light-bg` ou `--cream` em `custom.css`
|
||||||
|
- Exemplos de cores alternativas:
|
||||||
|
- `#F5F5DC` (bege mais escuro)
|
||||||
|
- `#FFFAF0` (creme floral)
|
||||||
|
- `#FDF5E6` (linho antigo)
|
||||||
|
|
||||||
|
3. **Logo personalizado:**
|
||||||
|
- Substitua `/wwwroot/img/logo-optimized.svg`
|
||||||
|
- Ou use seu logo original (se otimizar o SVG)
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
**Logo não aparece:**
|
||||||
|
- Verifique se `/wwwroot/img/logo-optimized.svg` existe
|
||||||
|
- Limpe o cache do browser (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
**Cores não mudaram:**
|
||||||
|
- Limpe cache do browser
|
||||||
|
- Verifique se `custom.css` está sendo carregado (DevTools → Network)
|
||||||
|
|
||||||
|
**Build falha:**
|
||||||
|
- Se a app está rodando, pare primeiro
|
||||||
|
- Execute: `dotnet clean && dotnet build`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Todas as mudanças solicitadas foram implementadas! ✅**
|
||||||
|
|
||||||
|
- ✅ Amarelo → Vermelho
|
||||||
|
- ✅ Logo com fundo transparente
|
||||||
|
- ✅ Menu com fundo claro (bege)
|
||||||
|
- ✅ Hover do portfolio corrigido (overlay vermelho cobrindo toda a área)
|
||||||
20
Models/CaseMetadata.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
namespace CarneiroTech.Models;
|
||||||
|
|
||||||
|
public class CaseMetadata
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Slug { get; set; } = string.Empty;
|
||||||
|
public string Summary { get; set; } = string.Empty;
|
||||||
|
public string Client { get; set; } = string.Empty;
|
||||||
|
public string Industry { get; set; } = string.Empty;
|
||||||
|
public string Timeline { get; set; } = string.Empty;
|
||||||
|
public string Role { get; set; } = string.Empty;
|
||||||
|
public string Image { get; set; } = string.Empty;
|
||||||
|
public List<string> Tags { get; set; } = new();
|
||||||
|
public bool Featured { get; set; }
|
||||||
|
public int Order { get; set; }
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public string SeoTitle { get; set; } = string.Empty;
|
||||||
|
public string SeoDescription { get; set; } = string.Empty;
|
||||||
|
public string SeoKeywords { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
8
Models/CaseModel.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace CarneiroTech.Models;
|
||||||
|
|
||||||
|
public class CaseModel
|
||||||
|
{
|
||||||
|
public CaseMetadata Metadata { get; set; } = new();
|
||||||
|
public string ContentHtml { get; set; } = string.Empty;
|
||||||
|
public string ContentMarkdown { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
20
Models/ContactFormModel.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Models;
|
||||||
|
|
||||||
|
public class ContactFormModel
|
||||||
|
{
|
||||||
|
[Required(ErrorMessage = "Nome é obrigatório")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Email é obrigatório")]
|
||||||
|
[EmailAddress(ErrorMessage = "Email inválido")]
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Telefone é obrigatório")]
|
||||||
|
[Phone(ErrorMessage = "Telefone inválido")]
|
||||||
|
public string Phone { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Mensagem é obrigatória")]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
8
Models/ErrorViewModel.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace CarneiroTech.Models;
|
||||||
|
|
||||||
|
public class ErrorViewModel
|
||||||
|
{
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
|
||||||
|
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
}
|
||||||
9
Models/SitemapItem.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace CarneiroTech.Models;
|
||||||
|
|
||||||
|
public class SitemapItem
|
||||||
|
{
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
public DateTime LastModified { get; set; }
|
||||||
|
public string ChangeFrequency { get; set; } = "weekly";
|
||||||
|
public decimal Priority { get; set; } = 0.5m;
|
||||||
|
}
|
||||||
36
Program.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using CarneiroTech.Services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add services to the container.
|
||||||
|
builder.Services.AddControllersWithViews();
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
// Register custom services
|
||||||
|
builder.Services.AddSingleton<IMarkdownService, MarkdownService>();
|
||||||
|
builder.Services.AddScoped<ICaseService, CaseService>();
|
||||||
|
builder.Services.AddScoped<ILanguageService, LanguageService>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (!app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseExceptionHandler("/Home/Error");
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "default",
|
||||||
|
pattern: "{controller=Home}/{action=Index}/{id?}");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
38
Properties/launchSettings.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:32693",
|
||||||
|
"sslPort": 44347
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5203",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7177;http://localhost:5203",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
340
QUICK_START.md
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
# Quick Start Guide - Carneiro Tech Website
|
||||||
|
|
||||||
|
## Immediate Next Steps
|
||||||
|
|
||||||
|
### 1. Test Locally (5 minutes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd aspnet/CarneiroTech
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open: `http://localhost:5000`
|
||||||
|
|
||||||
|
**What to check:**
|
||||||
|
- ✅ Homepage loads with all sections
|
||||||
|
- ✅ Click "Cases" in navbar → should show 3 cases
|
||||||
|
- ✅ Click on a case → should show full details
|
||||||
|
- ✅ Click on tags → should filter cases
|
||||||
|
- ✅ Sitemap at `/sitemap.xml` works
|
||||||
|
- ✅ Responsive design (resize browser)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Customize for Your Brand (30 minutes)
|
||||||
|
|
||||||
|
#### A. Update Logo Colors
|
||||||
|
|
||||||
|
Open `/wwwroot/css/custom.css` and update:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Find and replace #ffc800 with your primary color */
|
||||||
|
/* Find and replace #667eea and #764ba2 with your gradient colors */
|
||||||
|
```
|
||||||
|
|
||||||
|
To extract colors from your logo:
|
||||||
|
1. Open `/logo/LogoNovo.svg` in a browser
|
||||||
|
2. Use browser DevTools to inspect
|
||||||
|
3. Copy hex color codes
|
||||||
|
4. Replace in custom.css
|
||||||
|
|
||||||
|
#### B. Update LinkedIn URL
|
||||||
|
|
||||||
|
Open `/Views/Shared/_Layout.cshtml` (line ~93):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<a class="btn btn-dark btn-social mx-2"
|
||||||
|
href="https://linkedin.com/in/ricardo-carneiro" <!-- UPDATE THIS -->
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C. Update Domain URLs
|
||||||
|
|
||||||
|
Find and replace `carneirotech.com` with your actual domain:
|
||||||
|
|
||||||
|
Files to update:
|
||||||
|
- `/Views/Shared/_Layout.cshtml` (lines 12, 16, 47, 48)
|
||||||
|
- `/Controllers/HomeController.cs` (sitemap URLs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Add Your Own Cases (15 minutes per case)
|
||||||
|
|
||||||
|
#### Create New Case File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Content/Cases
|
||||||
|
# Create new file (use VSCode, Notepad, etc.)
|
||||||
|
touch my-new-case.md
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Copy Template
|
||||||
|
|
||||||
|
Use one of the existing cases as template:
|
||||||
|
- `sap-integration-healthcare.md` (technical integration)
|
||||||
|
- `legacy-modernization.md` (architecture/migration)
|
||||||
|
- `mvp-definition.md` (consulting/strategy)
|
||||||
|
|
||||||
|
#### Fill Front Matter
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "Your Project Title"
|
||||||
|
slug: "your-project-slug" # Will be URL: /cases/your-project-slug
|
||||||
|
summary: "Short description for cards (2-3 lines)"
|
||||||
|
client: "Client Name or Confidential"
|
||||||
|
industry: "Industry"
|
||||||
|
timeline: "X months"
|
||||||
|
role: "Your Role"
|
||||||
|
image: "" # Leave empty for gradient or add /img/cases/yourimage.jpg
|
||||||
|
tags:
|
||||||
|
- Tag1
|
||||||
|
- Tag2
|
||||||
|
- Tag3
|
||||||
|
featured: true # Shows on homepage
|
||||||
|
order: 1 # Lower number = higher priority
|
||||||
|
date: 2024-01-15 # YYYY-MM-DD format
|
||||||
|
seo_title: "SEO Title (max 60 chars)"
|
||||||
|
seo_description: "SEO description (max 160 chars)"
|
||||||
|
seo_keywords: "keyword1, keyword2, keyword3"
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Write Content
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Brief overview...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Challenge
|
||||||
|
|
||||||
|
What was the problem?
|
||||||
|
|
||||||
|
### Pain Points:
|
||||||
|
- Point 1
|
||||||
|
- Point 2
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
How did you solve it?
|
||||||
|
|
||||||
|
\`\`\`csharp
|
||||||
|
// Code examples work
|
||||||
|
public void Example() {}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
Metrics and outcomes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
\`Tag1\` \`Tag2\` \`Tag3\`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Call to action link](/#contact)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Preview
|
||||||
|
|
||||||
|
Restart app or wait 60min (cache expires):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
# Navigate to: http://localhost:5000/cases/your-project-slug
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Configure Email (30 minutes)
|
||||||
|
|
||||||
|
For the contact form to work, you need to implement email sending.
|
||||||
|
|
||||||
|
#### Option A: SendGrid (Recommended)
|
||||||
|
|
||||||
|
1. Sign up at sendgrid.com
|
||||||
|
2. Get API key
|
||||||
|
3. Add NuGet package:
|
||||||
|
```bash
|
||||||
|
dotnet add package SendGrid
|
||||||
|
```
|
||||||
|
4. Update `HomeController.cs` Contact method:
|
||||||
|
```csharp
|
||||||
|
// Replace TODO with SendGrid code
|
||||||
|
var apiKey = Configuration["SendGrid:ApiKey"];
|
||||||
|
var client = new SendGridClient(apiKey);
|
||||||
|
var from = new EmailAddress("noreply@carneirotech.com", "Carneiro Tech");
|
||||||
|
var subject = $"Novo contato: {model.Name}";
|
||||||
|
var to = new EmailAddress("ricardo@carneirotech.com");
|
||||||
|
var plainTextContent = model.Message;
|
||||||
|
var htmlContent = $"<strong>Nome:</strong> {model.Name}<br>...";
|
||||||
|
var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent);
|
||||||
|
await client.SendEmailAsync(msg);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: SMTP (Gmail, Outlook, etc.)
|
||||||
|
|
||||||
|
1. Add NuGet package:
|
||||||
|
```bash
|
||||||
|
dotnet add package MailKit
|
||||||
|
```
|
||||||
|
2. Configure SMTP settings in appsettings.json
|
||||||
|
3. Implement SMTP sending logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Add Portfolio Images (Optional)
|
||||||
|
|
||||||
|
1. Create folder: `/wwwroot/img/cases/`
|
||||||
|
2. Add your images (jpg, png, webp)
|
||||||
|
3. Update case markdown front matter:
|
||||||
|
```yaml
|
||||||
|
image: "/img/cases/my-project.jpg"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommended image size:** 800x600px (4:3 ratio)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Docker Deployment (Production)
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd aspnet/CarneiroTech
|
||||||
|
docker build -t carneirotech:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
-p 80:80 \
|
||||||
|
-p 443:443 \
|
||||||
|
--name carneirotech \
|
||||||
|
-v $(pwd)/Content:/app/Content:ro \
|
||||||
|
carneirotech:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Or use Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access:** `http://localhost:8080`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues & Solutions
|
||||||
|
|
||||||
|
### Issue: "404 Not Found" on case pages
|
||||||
|
|
||||||
|
**Solution:** Check slug in markdown matches URL
|
||||||
|
- Markdown: `slug: "my-case"`
|
||||||
|
- URL: `/cases/my-case` (must match exactly)
|
||||||
|
|
||||||
|
### Issue: Cases not appearing
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check markdown file is in `Content/Cases/`
|
||||||
|
2. Verify front matter YAML is valid
|
||||||
|
3. Restart app (cache clears)
|
||||||
|
4. Check date format: `YYYY-MM-DD`
|
||||||
|
|
||||||
|
### Issue: Build fails
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
dotnet clean
|
||||||
|
dotnet restore
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: CSS not loading
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check file exists: `/wwwroot/css/custom.css`
|
||||||
|
2. Verify `_Layout.cshtml` includes it
|
||||||
|
3. Clear browser cache (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
### Issue: Logo not appearing
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Check file: `/wwwroot/img/logo.svg`
|
||||||
|
2. Verify navbar reference in `_Layout.cshtml`
|
||||||
|
3. Check image dimensions (logo should be ~40px height)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Enable Response Caching**
|
||||||
|
- Add to `Program.cs`: `builder.Services.AddResponseCaching();`
|
||||||
|
- Add to pipeline: `app.UseResponseCaching();`
|
||||||
|
|
||||||
|
2. **Enable Response Compression**
|
||||||
|
- Add to `Program.cs`: `builder.Services.AddResponseCompression();`
|
||||||
|
|
||||||
|
3. **Optimize Images**
|
||||||
|
- Use WebP format
|
||||||
|
- Compress before uploading
|
||||||
|
- Tools: TinyPNG, Squoosh
|
||||||
|
|
||||||
|
4. **CDN for Static Assets**
|
||||||
|
- Upload `/wwwroot/` to CDN
|
||||||
|
- Update references in `_Layout.cshtml`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Update all `carneirotech.com` URLs to your domain
|
||||||
|
- [ ] Configure SSL certificate (Let's Encrypt recommended)
|
||||||
|
- [ ] Set `ASPNETCORE_ENVIRONMENT=Production`
|
||||||
|
- [ ] Configure email sending (SendGrid or SMTP)
|
||||||
|
- [ ] Add Google Analytics (optional)
|
||||||
|
- [ ] Test all pages
|
||||||
|
- [ ] Test contact form
|
||||||
|
- [ ] Verify sitemap: `/sitemap.xml`
|
||||||
|
- [ ] Verify robots.txt: `/robots.txt`
|
||||||
|
- [ ] Test responsive design
|
||||||
|
- [ ] Run Lighthouse audit
|
||||||
|
- [ ] Set up monitoring (e.g., Application Insights)
|
||||||
|
- [ ] Configure backup for Content folder
|
||||||
|
- [ ] Test Docker deployment
|
||||||
|
- [ ] Set up CI/CD (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **README.md** - Full documentation
|
||||||
|
- **IMPLEMENTATION_SUMMARY.md** - What was built
|
||||||
|
- **Example Cases** - 3 markdown examples in `Content/Cases/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
If you encounter issues:
|
||||||
|
|
||||||
|
1. Check the README.md for detailed documentation
|
||||||
|
2. Review IMPLEMENTATION_SUMMARY.md for architecture
|
||||||
|
3. Look at example cases for markdown syntax
|
||||||
|
4. Check Controller code for logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to launch in ~2 hours** (including customizations)
|
||||||
|
|
||||||
|
Good luck! 🚀
|
||||||
303
README.md
@ -1,2 +1,303 @@
|
|||||||
# CarneiroTech
|
# Carneiro Tech - Professional Consulting Website
|
||||||
|
|
||||||
|
Professional website for Carneiro Tech - Solution Design & Technical Consulting, built with ASP.NET MVC Core and Bootstrap.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Modern Design**: Agency Bootstrap template adapted for consulting
|
||||||
|
- **Markdown-based Cases**: Easy portfolio management with .md files
|
||||||
|
- **SEO Optimized**: Meta tags, Open Graph, Twitter Cards, JSON-LD structured data
|
||||||
|
- **Responsive**: Mobile-first design using Bootstrap 5
|
||||||
|
- **Tag Filtering**: Filter cases by technology/category
|
||||||
|
- **Sitemap**: Automatic XML sitemap generation
|
||||||
|
- **Docker Ready**: Containerized for easy deployment
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- ASP.NET MVC Core 8
|
||||||
|
- C# 12
|
||||||
|
- Markdig (Markdown parsing)
|
||||||
|
- YamlDotNet (YAML front matter)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Bootstrap 5 (Agency template)
|
||||||
|
- Font Awesome icons
|
||||||
|
- Google Fonts (Montserrat, Roboto Slab)
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Ready for OCI (Oracle Cloud Infrastructure)
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
CarneiroTech/
|
||||||
|
├── Controllers/
|
||||||
|
│ ├── HomeController.cs # Homepage, sitemap, contact
|
||||||
|
│ └── CasesController.cs # Cases list and details
|
||||||
|
├── Models/
|
||||||
|
│ ├── CaseModel.cs
|
||||||
|
│ ├── CaseMetadata.cs
|
||||||
|
│ ├── ContactFormModel.cs
|
||||||
|
│ └── SitemapItem.cs
|
||||||
|
├── Services/
|
||||||
|
│ ├── ICaseService.cs
|
||||||
|
│ ├── CaseService.cs
|
||||||
|
│ ├── IMarkdownService.cs
|
||||||
|
│ └── MarkdownService.cs
|
||||||
|
├── Views/
|
||||||
|
│ ├── Home/
|
||||||
|
│ │ └── Index.cshtml # Homepage
|
||||||
|
│ ├── Cases/
|
||||||
|
│ │ ├── Index.cshtml # Cases list
|
||||||
|
│ │ └── Details.cshtml # Individual case
|
||||||
|
│ └── Shared/
|
||||||
|
│ └── _Layout.cshtml # Main layout with SEO
|
||||||
|
├── Content/
|
||||||
|
│ └── Cases/ # Markdown case files
|
||||||
|
│ ├── sap-integration-healthcare.md
|
||||||
|
│ ├── legacy-modernization.md
|
||||||
|
│ └── mvp-definition.md
|
||||||
|
├── wwwroot/
|
||||||
|
│ ├── css/ # Bootstrap template CSS
|
||||||
|
│ ├── js/ # Bootstrap template JS
|
||||||
|
│ ├── img/ # Images and logo
|
||||||
|
│ └── robots.txt
|
||||||
|
├── Dockerfile
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||||
|
- [Docker](https://www.docker.com/get-started) (optional, for containerized deployment)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd CarneiroTech
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Restore dependencies**
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run the application**
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open in browser**
|
||||||
|
Navigate to: `http://localhost:5000` or `https://localhost:5001`
|
||||||
|
|
||||||
|
### Running with Docker
|
||||||
|
|
||||||
|
1. **Build and run with Docker Compose**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Access the application**
|
||||||
|
Navigate to: `http://localhost:8080`
|
||||||
|
|
||||||
|
3. **Stop the container**
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Cases
|
||||||
|
|
||||||
|
### 1. Create a Markdown File
|
||||||
|
|
||||||
|
Create a new file in `Content/Cases/` folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch Content/Cases/my-new-case.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Add Front Matter + Content
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: "My Amazing Project"
|
||||||
|
slug: "my-amazing-project"
|
||||||
|
summary: "Short summary for cards and SEO"
|
||||||
|
client: "Client Name"
|
||||||
|
industry: "Industry"
|
||||||
|
timeline: "3 months"
|
||||||
|
role: "Your Role"
|
||||||
|
image: "/img/cases/project.jpg"
|
||||||
|
tags:
|
||||||
|
- Tag1
|
||||||
|
- Tag2
|
||||||
|
- Tag3
|
||||||
|
featured: true
|
||||||
|
order: 1
|
||||||
|
date: 2024-01-15
|
||||||
|
seo_title: "SEO optimized title (max 60 chars)"
|
||||||
|
seo_description: "SEO description (max 160 chars)"
|
||||||
|
seo_keywords: "keyword1, keyword2, keyword3"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Your case content here in Markdown format...
|
||||||
|
|
||||||
|
### Subsection
|
||||||
|
|
||||||
|
More content...
|
||||||
|
|
||||||
|
- Bullet points
|
||||||
|
- Work great
|
||||||
|
|
||||||
|
\`\`\`csharp
|
||||||
|
// Code blocks work too
|
||||||
|
public void Example() {
|
||||||
|
Console.WriteLine("Hello!");
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Refresh the Application
|
||||||
|
|
||||||
|
The case service uses in-memory caching (60 minutes). Either:
|
||||||
|
- Wait for cache expiration
|
||||||
|
- Restart the application
|
||||||
|
- Implement cache invalidation
|
||||||
|
|
||||||
|
### 4. Access Your Case
|
||||||
|
|
||||||
|
Navigate to: `/cases/my-amazing-project`
|
||||||
|
|
||||||
|
## Markdown Features
|
||||||
|
|
||||||
|
Supported Markdown features:
|
||||||
|
- **Headers** (H1-H6)
|
||||||
|
- **Bold**, *italic*, ~~strikethrough~~
|
||||||
|
- Lists (ordered and unordered)
|
||||||
|
- Links and images
|
||||||
|
- Code blocks with syntax highlighting
|
||||||
|
- Tables
|
||||||
|
- Blockquotes
|
||||||
|
- Horizontal rules
|
||||||
|
|
||||||
|
## SEO Features
|
||||||
|
|
||||||
|
### Meta Tags
|
||||||
|
- Dynamic title, description, keywords per page
|
||||||
|
- Canonical URLs
|
||||||
|
- Author meta tag
|
||||||
|
|
||||||
|
### Open Graph
|
||||||
|
- Full OG tags for social sharing
|
||||||
|
- Dynamic OG images per case
|
||||||
|
- Locale support (pt_BR)
|
||||||
|
|
||||||
|
### Twitter Cards
|
||||||
|
- Summary cards with large images
|
||||||
|
- Dynamic content per page
|
||||||
|
|
||||||
|
### Structured Data
|
||||||
|
- JSON-LD Organization schema
|
||||||
|
- Professional service markup
|
||||||
|
- Enhanced search results
|
||||||
|
|
||||||
|
### Sitemap
|
||||||
|
- Auto-generated XML sitemap
|
||||||
|
- Accessible at `/sitemap.xml`
|
||||||
|
- Includes homepage, cases index, and all individual cases
|
||||||
|
|
||||||
|
### Robots.txt
|
||||||
|
- Located at `/robots.txt`
|
||||||
|
- Allows all crawlers
|
||||||
|
- Points to sitemap
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
Cases are cached in memory for 60 minutes. To adjust:
|
||||||
|
|
||||||
|
Edit `Services/CaseService.cs`:
|
||||||
|
```csharp
|
||||||
|
private const int CACHE_MINUTES = 60; // Change this value
|
||||||
|
```
|
||||||
|
|
||||||
|
### Site URL
|
||||||
|
|
||||||
|
Update the canonical URL and sitemap URLs in:
|
||||||
|
- `Views/Shared/_Layout.cshtml` (line 12)
|
||||||
|
- `Controllers/HomeController.cs` (Sitemap method)
|
||||||
|
|
||||||
|
Replace `https://carneirotech.com` with your domain.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
1. **Build the image**
|
||||||
|
```bash
|
||||||
|
docker build -t carneirotech:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run the container**
|
||||||
|
```bash
|
||||||
|
docker run -d -p 8080:80 --name carneirotech carneirotech:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### OCI (Oracle Cloud) Deployment
|
||||||
|
|
||||||
|
1. Push image to OCI Container Registry
|
||||||
|
2. Create Container Instance
|
||||||
|
3. Configure port mapping (80/443)
|
||||||
|
4. Set environment variables
|
||||||
|
5. Mount volume for Content folder (optional)
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Logo
|
||||||
|
|
||||||
|
Replace `/wwwroot/img/logo.svg` with your logo.
|
||||||
|
|
||||||
|
Update navbar logo reference in `_Layout.cshtml` if needed.
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
The template uses Bootstrap 5 with custom colors. To customize:
|
||||||
|
|
||||||
|
Edit `/wwwroot/css/styles.css`:
|
||||||
|
- Primary color: `#ffc800` (yellow/gold)
|
||||||
|
- Dark color: `#212529`
|
||||||
|
- Fonts: Montserrat, Roboto Slab
|
||||||
|
|
||||||
|
### Content
|
||||||
|
|
||||||
|
Edit the following views to customize content:
|
||||||
|
- `Views/Home/Index.cshtml` - Homepage content
|
||||||
|
- `Views/Shared/_Layout.cshtml` - Navigation, footer
|
||||||
|
- `Controllers/HomeController.cs` - Contact form logic
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is private and proprietary.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, contact: ricardo@carneirotech.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Built with ❤️ for Carneiro Tech**
|
||||||
|
|||||||
99
Resources/SiteStrings.cs
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
namespace CarneiroTech.Resources
|
||||||
|
{
|
||||||
|
public class SiteStrings
|
||||||
|
{
|
||||||
|
public static Dictionary<string, Dictionary<string, string>> Translations = new()
|
||||||
|
{
|
||||||
|
// Navigation
|
||||||
|
["nav.services"] = new() { ["pt"] = "Serviços", ["en"] = "Services", ["es"] = "Servicios" },
|
||||||
|
["nav.cases"] = new() { ["pt"] = "Cases", ["en"] = "Cases", ["es"] = "Casos" },
|
||||||
|
["nav.about"] = new() { ["pt"] = "Sobre", ["en"] = "About", ["es"] = "Acerca de" },
|
||||||
|
["nav.contact"] = new() { ["pt"] = "Contato", ["en"] = "Contact", ["es"] = "Contacto" },
|
||||||
|
|
||||||
|
// Hero Section
|
||||||
|
["hero.greeting"] = new() { ["pt"] = "Bem-vindo ao Carneiro Tech", ["en"] = "Welcome to Carneiro Tech", ["es"] = "Bienvenido a Carneiro Tech" },
|
||||||
|
["hero.tagline"] = new() { ["pt"] = "É um prazer ter você aqui!", ["en"] = "It's A Great Pleasure To Have You Here!", ["es"] = "¡Es un placer tenerlo aquí!" },
|
||||||
|
["hero.title"] = new() { ["pt"] = "Solution Design & Consultoria Técnica", ["en"] = "Solution Design & Technical Consulting", ["es"] = "Solution Design y Consultoría Técnica" },
|
||||||
|
["hero.cta"] = new() { ["pt"] = "Saiba Mais", ["en"] = "Learn More", ["es"] = "Más Información" },
|
||||||
|
|
||||||
|
// Services Section
|
||||||
|
["services.title"] = new() { ["pt"] = "Serviços", ["en"] = "Services", ["es"] = "Servicios" },
|
||||||
|
["services.subtitle"] = new() { ["pt"] = "Transformando desafios técnicos em soluções eficientes", ["en"] = "Transforming technical challenges into efficient solutions", ["es"] = "Transformando desafíos técnicos en soluciones eficientes" },
|
||||||
|
|
||||||
|
["service.solution.title"] = new() { ["pt"] = "Solution Design", ["en"] = "Solution Design", ["es"] = "Solution Design" },
|
||||||
|
["service.solution.desc"] = new() { ["pt"] = "Desenho completo de soluções técnicas, identificando requisitos não explícitos e propondo arquiteturas adequadas ao contexto do projeto.", ["en"] = "Complete technical solution design, identifying non-explicit requirements and proposing architectures suitable for the project context.", ["es"] = "Diseño completo de soluciones técnicas, identificando requisitos no explícitos y proponiendo arquitecturas adecuadas al contexto del proyecto." },
|
||||||
|
|
||||||
|
["service.modernization.title"] = new() { ["pt"] = "Modernização de Sistemas", ["en"] = "System Modernization", ["es"] = "Modernización de Sistemas" },
|
||||||
|
["service.modernization.desc"] = new() { ["pt"] = "Migração de aplicações legadas para tecnologias modernas, com estratégias que garantem continuidade operacional e zero downtime.", ["en"] = "Migration of legacy applications to modern technologies, with strategies that ensure operational continuity and zero downtime.", ["es"] = "Migración de aplicaciones heredadas a tecnologías modernas, con estrategias que garantizan continuidad operativa y cero tiempo de inactividad." },
|
||||||
|
|
||||||
|
["service.architecture.title"] = new() { ["pt"] = "Arquitetura de Software", ["en"] = "Software Architecture", ["es"] = "Arquitectura de Software" },
|
||||||
|
["service.architecture.desc"] = new() { ["pt"] = "Definição de arquiteturas escaláveis, resilientes e adequadas ao contexto, considerando custos, prazos e manutenibilidade.", ["en"] = "Definition of scalable, resilient and context-appropriate architectures, considering costs, deadlines and maintainability.", ["es"] = "Definición de arquitecturas escalables, resilientes y adecuadas al contexto, considerando costos, plazos y mantenibilidad." },
|
||||||
|
|
||||||
|
["service.consulting.title"] = new() { ["pt"] = "Consultoria Técnica", ["en"] = "Technical Consulting", ["es"] = "Consultoría Técnica" },
|
||||||
|
["service.consulting.desc"] = new() { ["pt"] = "Análise de viabilidade técnica, otimização de processos, redução de custos operacionais e escolha de tecnologias adequadas.", ["en"] = "Technical feasibility analysis, process optimization, operational cost reduction and selection of appropriate technologies.", ["es"] = "Análisis de viabilidad técnica, optimización de procesos, reducción de costos operativos y selección de tecnologías adecuadas." },
|
||||||
|
|
||||||
|
// Portfolio/Cases Section
|
||||||
|
["portfolio.title"] = new() { ["pt"] = "Cases de Sucesso", ["en"] = "Success Cases", ["es"] = "Casos de Éxito" },
|
||||||
|
["portfolio.subtitle"] = new() { ["pt"] = "Conheça alguns projetos onde transformei desafios complexos em soluções elegantes", ["en"] = "Discover some projects where I transformed complex challenges into elegant solutions", ["es"] = "Conozca algunos proyectos donde transformé desafíos complejos en soluciones elegantes" },
|
||||||
|
["portfolio.viewcase"] = new() { ["pt"] = "Ver Case Completo", ["en"] = "View Full Case", ["es"] = "Ver Caso Completo" },
|
||||||
|
|
||||||
|
// About Section
|
||||||
|
["about.title"] = new() { ["pt"] = "Sobre", ["en"] = "About", ["es"] = "Acerca de" },
|
||||||
|
["about.subtitle"] = new() { ["pt"] = "Mais de uma década transformando desafios técnicos em soluções eficientes", ["en"] = "Over a decade transforming technical challenges into efficient solutions", ["es"] = "Más de una década transformando desafíos técnicos en soluciones eficientes" },
|
||||||
|
["about.text"] = new() {
|
||||||
|
["pt"] = "Com mais de 10 anos de experiência em engenharia de software, especializei-me em Solution Design, arquitetura de sistemas e modernização de aplicações legadas. Meu diferencial está em identificar requisitos não explícitos, antecipar problemas e propor soluções que realmente funcionam na prática.",
|
||||||
|
["en"] = "With over 10 years of experience in software engineering, I specialize in Solution Design, systems architecture and legacy application modernization. My differential is in identifying non-explicit requirements, anticipating problems and proposing solutions that really work in practice.",
|
||||||
|
["es"] = "Con más de 10 años de experiencia en ingeniería de software, me especializo en Solution Design, arquitectura de sistemas y modernización de aplicaciones heredadas. Mi diferencial está en identificar requisitos no explícitos, anticipar problemas y proponer soluciones que realmente funcionan en la práctica."
|
||||||
|
},
|
||||||
|
|
||||||
|
// Contact Section
|
||||||
|
["contact.title"] = new() { ["pt"] = "Contato", ["en"] = "Contact", ["es"] = "Contacto" },
|
||||||
|
["contact.subtitle"] = new() { ["pt"] = "Vamos conversar sobre seu desafio técnico", ["en"] = "Let's talk about your technical challenge", ["es"] = "Hablemos sobre su desafío técnico" },
|
||||||
|
["contact.name"] = new() { ["pt"] = "Seu Nome *", ["en"] = "Your Name *", ["es"] = "Su Nombre *" },
|
||||||
|
["contact.email"] = new() { ["pt"] = "Seu Email *", ["en"] = "Your Email *", ["es"] = "Su Email *" },
|
||||||
|
["contact.phone"] = new() { ["pt"] = "Seu Telefone", ["en"] = "Your Phone", ["es"] = "Su Teléfono" },
|
||||||
|
["contact.message"] = new() { ["pt"] = "Sua Mensagem *", ["en"] = "Your Message *", ["es"] = "Su Mensaje *" },
|
||||||
|
["contact.send"] = new() { ["pt"] = "Enviar Mensagem", ["en"] = "Send Message", ["es"] = "Enviar Mensaje" },
|
||||||
|
["contact.sending"] = new() { ["pt"] = "Enviando...", ["en"] = "Sending...", ["es"] = "Enviando..." },
|
||||||
|
["contact.success"] = new() { ["pt"] = "Mensagem enviada com sucesso! Entraremos em contato em breve.", ["en"] = "Message sent successfully! We'll get in touch soon.", ["es"] = "¡Mensaje enviado con éxito! Nos pondremos en contacto pronto." },
|
||||||
|
["contact.error"] = new() { ["pt"] = "Erro ao enviar mensagem. Por favor, tente novamente ou entre em contato via email.", ["en"] = "Error sending message. Please try again or contact via email.", ["es"] = "Error al enviar mensaje. Por favor, inténtelo de nuevo o contacte por email." },
|
||||||
|
["contact.or"] = new() { ["pt"] = "OU", ["en"] = "OR", ["es"] = "O" },
|
||||||
|
["contact.whatsapp"] = new() { ["pt"] = "Falar via WhatsApp", ["en"] = "Chat on WhatsApp", ["es"] = "Hablar por WhatsApp" },
|
||||||
|
["contact.whatsapp.subtitle"] = new() { ["pt"] = "Resposta rápida e direta", ["en"] = "Quick and direct response", ["es"] = "Respuesta rápida y directa" },
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
["footer.rights"] = new() { ["pt"] = "Carneiro Tech - Todos os direitos reservados", ["en"] = "Carneiro Tech - All rights reserved", ["es"] = "Carneiro Tech - Todos los derechos reservados" },
|
||||||
|
["footer.privacy"] = new() { ["pt"] = "Privacidade", ["en"] = "Privacy", ["es"] = "Privacidad" },
|
||||||
|
["footer.terms"] = new() { ["pt"] = "Termos", ["en"] = "Terms", ["es"] = "Términos" },
|
||||||
|
|
||||||
|
// Case Details
|
||||||
|
["case.client"] = new() { ["pt"] = "Cliente", ["en"] = "Client", ["es"] = "Cliente" },
|
||||||
|
["case.industry"] = new() { ["pt"] = "Indústria", ["en"] = "Industry", ["es"] = "Industria" },
|
||||||
|
["case.timeline"] = new() { ["pt"] = "Prazo", ["en"] = "Timeline", ["es"] = "Plazo" },
|
||||||
|
["case.role"] = new() { ["pt"] = "Papel", ["en"] = "Role", ["es"] = "Rol" },
|
||||||
|
["case.tags"] = new() { ["pt"] = "Tecnologias", ["en"] = "Technologies", ["es"] = "Tecnologías" },
|
||||||
|
["case.back"] = new() { ["pt"] = "← Voltar para Cases", ["en"] = "← Back to Cases", ["es"] = "← Volver a Casos" },
|
||||||
|
|
||||||
|
// WhatsApp message
|
||||||
|
["whatsapp.message"] = new() {
|
||||||
|
["pt"] = "[Site] Olá! Eu gostaria de conversar com você sobre uma possível proposta comercial.",
|
||||||
|
["en"] = "[Website] Hello! I would like to talk to you about a possible business proposal.",
|
||||||
|
["es"] = "[Sitio] ¡Hola! Me gustaría hablar con usted sobre una posible propuesta comercial."
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Get(string key, string language)
|
||||||
|
{
|
||||||
|
if (Translations.TryGetValue(key, out var translations))
|
||||||
|
{
|
||||||
|
if (translations.TryGetValue(language, out var text))
|
||||||
|
{
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
// Fallback to Portuguese if translation not found
|
||||||
|
return translations.GetValueOrDefault("pt", key);
|
||||||
|
}
|
||||||
|
return key; // Return key if not found at all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
Services/CaseService.cs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
using CarneiroTech.Models;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Services;
|
||||||
|
|
||||||
|
public class CaseService : ICaseService
|
||||||
|
{
|
||||||
|
private readonly IMarkdownService _markdownService;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly IWebHostEnvironment _environment;
|
||||||
|
private readonly ILanguageService _languageService;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly string _casesBasePath;
|
||||||
|
private const string CACHE_KEY_PREFIX = "cases_";
|
||||||
|
private const int CACHE_MINUTES = 60;
|
||||||
|
|
||||||
|
public CaseService(IMarkdownService markdownService, IMemoryCache cache, IWebHostEnvironment environment,
|
||||||
|
ILanguageService languageService, IHttpContextAccessor httpContextAccessor)
|
||||||
|
{
|
||||||
|
_markdownService = markdownService;
|
||||||
|
_cache = cache;
|
||||||
|
_environment = environment;
|
||||||
|
_languageService = languageService;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_casesBasePath = Path.Combine(_environment.ContentRootPath, "Content", "Cases");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCurrentLanguage()
|
||||||
|
{
|
||||||
|
if (_httpContextAccessor.HttpContext != null)
|
||||||
|
{
|
||||||
|
return _languageService.GetCurrentLanguage(_httpContextAccessor.HttpContext);
|
||||||
|
}
|
||||||
|
return "pt"; // Default fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCasesPath(string language)
|
||||||
|
{
|
||||||
|
return Path.Combine(_casesBasePath, language);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CaseModel>> GetAllCasesAsync()
|
||||||
|
{
|
||||||
|
var language = GetCurrentLanguage();
|
||||||
|
var cacheKey = $"{CACHE_KEY_PREFIX}{language}";
|
||||||
|
|
||||||
|
if (_cache.TryGetValue(cacheKey, out List<CaseModel>? cachedCases) && cachedCases != null)
|
||||||
|
{
|
||||||
|
return cachedCases;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cases = new List<CaseModel>();
|
||||||
|
var casesPath = GetCasesPath(language);
|
||||||
|
|
||||||
|
if (!Directory.Exists(casesPath))
|
||||||
|
{
|
||||||
|
// Try Portuguese as fallback
|
||||||
|
casesPath = GetCasesPath("pt");
|
||||||
|
if (!Directory.Exists(casesPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(casesPath);
|
||||||
|
return cases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var markdownFiles = Directory.GetFiles(casesPath, "*.md");
|
||||||
|
|
||||||
|
foreach (var file in markdownFiles)
|
||||||
|
{
|
||||||
|
var caseModel = await ParseCaseFileAsync(file);
|
||||||
|
if (caseModel != null)
|
||||||
|
{
|
||||||
|
cases.Add(caseModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by order (ascending) and date (descending)
|
||||||
|
cases = cases.OrderBy(c => c.Metadata.Order)
|
||||||
|
.ThenByDescending(c => c.Metadata.Date)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_cache.Set(cacheKey, cases, TimeSpan.FromMinutes(CACHE_MINUTES));
|
||||||
|
|
||||||
|
return cases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CaseModel>> GetFeaturedCasesAsync()
|
||||||
|
{
|
||||||
|
var allCases = await GetAllCasesAsync();
|
||||||
|
return allCases.Where(c => c.Metadata.Featured).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CaseModel?> GetCaseBySlugAsync(string slug)
|
||||||
|
{
|
||||||
|
var allCases = await GetAllCasesAsync();
|
||||||
|
return allCases.FirstOrDefault(c => c.Metadata.Slug.Equals(slug, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetAllTagsAsync()
|
||||||
|
{
|
||||||
|
var allCases = await GetAllCasesAsync();
|
||||||
|
var tags = allCases.SelectMany(c => c.Metadata.Tags)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(t => t)
|
||||||
|
.ToList();
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<CaseModel>> GetCasesByTagAsync(string tag)
|
||||||
|
{
|
||||||
|
var allCases = await GetAllCasesAsync();
|
||||||
|
return allCases.Where(c => c.Metadata.Tags.Contains(tag, StringComparer.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CaseModel?> ParseCaseFileAsync(string filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = await File.ReadAllTextAsync(filePath);
|
||||||
|
var frontMatter = _markdownService.ParseFrontMatter(content, out string bodyContent);
|
||||||
|
|
||||||
|
var metadata = new CaseMetadata
|
||||||
|
{
|
||||||
|
Title = GetStringValue(frontMatter, "title"),
|
||||||
|
Slug = GetStringValue(frontMatter, "slug"),
|
||||||
|
Summary = GetStringValue(frontMatter, "summary"),
|
||||||
|
Client = GetStringValue(frontMatter, "client"),
|
||||||
|
Industry = GetStringValue(frontMatter, "industry"),
|
||||||
|
Timeline = GetStringValue(frontMatter, "timeline"),
|
||||||
|
Role = GetStringValue(frontMatter, "role"),
|
||||||
|
Image = GetStringValue(frontMatter, "image"),
|
||||||
|
Tags = GetListValue(frontMatter, "tags"),
|
||||||
|
Featured = GetBoolValue(frontMatter, "featured"),
|
||||||
|
Order = GetIntValue(frontMatter, "order"),
|
||||||
|
Date = GetDateValue(frontMatter, "date"),
|
||||||
|
SeoTitle = GetStringValue(frontMatter, "seo_title"),
|
||||||
|
SeoDescription = GetStringValue(frontMatter, "seo_description"),
|
||||||
|
SeoKeywords = GetStringValue(frontMatter, "seo_keywords")
|
||||||
|
};
|
||||||
|
|
||||||
|
var htmlContent = _markdownService.ConvertToHtml(bodyContent);
|
||||||
|
|
||||||
|
return new CaseModel
|
||||||
|
{
|
||||||
|
Metadata = metadata,
|
||||||
|
ContentHtml = htmlContent,
|
||||||
|
ContentMarkdown = bodyContent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Log error here if needed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStringValue(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
return dict.ContainsKey(key) ? dict[key]?.ToString() ?? string.Empty : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetListValue(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
if (!dict.ContainsKey(key)) return new List<string>();
|
||||||
|
|
||||||
|
if (dict[key] is List<object> list)
|
||||||
|
{
|
||||||
|
return list.Select(item => item?.ToString() ?? string.Empty).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool GetBoolValue(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
if (!dict.ContainsKey(key)) return false;
|
||||||
|
|
||||||
|
var value = dict[key]?.ToString()?.ToLower();
|
||||||
|
return value == "true" || value == "yes" || value == "1";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetIntValue(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
if (!dict.ContainsKey(key)) return 0;
|
||||||
|
|
||||||
|
if (int.TryParse(dict[key]?.ToString(), out int result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime GetDateValue(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
if (!dict.ContainsKey(key)) return DateTime.MinValue;
|
||||||
|
|
||||||
|
var dateString = dict[key]?.ToString();
|
||||||
|
if (DateTime.TryParse(dateString, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.MinValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Services/ICaseService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using CarneiroTech.Models;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Services;
|
||||||
|
|
||||||
|
public interface ICaseService
|
||||||
|
{
|
||||||
|
Task<List<CaseModel>> GetAllCasesAsync();
|
||||||
|
Task<List<CaseModel>> GetFeaturedCasesAsync();
|
||||||
|
Task<CaseModel?> GetCaseBySlugAsync(string slug);
|
||||||
|
Task<List<string>> GetAllTagsAsync();
|
||||||
|
Task<List<CaseModel>> GetCasesByTagAsync(string tag);
|
||||||
|
}
|
||||||
7
Services/IMarkdownService.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace CarneiroTech.Services;
|
||||||
|
|
||||||
|
public interface IMarkdownService
|
||||||
|
{
|
||||||
|
string ConvertToHtml(string markdown);
|
||||||
|
Dictionary<string, object> ParseFrontMatter(string content, out string bodyContent);
|
||||||
|
}
|
||||||
90
Services/LanguageService.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Services
|
||||||
|
{
|
||||||
|
public interface ILanguageService
|
||||||
|
{
|
||||||
|
string GetCurrentLanguage(HttpContext context);
|
||||||
|
void SetLanguage(HttpContext context, string language);
|
||||||
|
string DetectBrowserLanguage(HttpContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LanguageService : ILanguageService
|
||||||
|
{
|
||||||
|
private const string LanguageCookieName = "CarneiroTech_Language";
|
||||||
|
private readonly string[] _supportedLanguages = { "pt", "en", "es" };
|
||||||
|
private const string DefaultLanguage = "pt";
|
||||||
|
|
||||||
|
public string GetCurrentLanguage(HttpContext context)
|
||||||
|
{
|
||||||
|
// 1. Check cookie first (user preference)
|
||||||
|
if (context.Request.Cookies.TryGetValue(LanguageCookieName, out var cookieLanguage))
|
||||||
|
{
|
||||||
|
if (_supportedLanguages.Contains(cookieLanguage))
|
||||||
|
{
|
||||||
|
return cookieLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Detect from browser Accept-Language header
|
||||||
|
var browserLanguage = DetectBrowserLanguage(context);
|
||||||
|
if (!string.IsNullOrEmpty(browserLanguage))
|
||||||
|
{
|
||||||
|
// Save detected language to cookie
|
||||||
|
SetLanguage(context, browserLanguage);
|
||||||
|
return browserLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to default (Portuguese)
|
||||||
|
return DefaultLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLanguage(HttpContext context, string language)
|
||||||
|
{
|
||||||
|
if (_supportedLanguages.Contains(language))
|
||||||
|
{
|
||||||
|
var cookieOptions = new CookieOptions
|
||||||
|
{
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddYears(1),
|
||||||
|
HttpOnly = false, // Allow JavaScript to read for client-side logic
|
||||||
|
Secure = true,
|
||||||
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Path = "/"
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Response.Cookies.Append(LanguageCookieName, language, cookieOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DetectBrowserLanguage(HttpContext context)
|
||||||
|
{
|
||||||
|
var acceptLanguageHeader = context.Request.Headers["Accept-Language"].ToString();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(acceptLanguageHeader))
|
||||||
|
{
|
||||||
|
return DefaultLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Accept-Language header
|
||||||
|
// Format: "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6"
|
||||||
|
var browserLanguages = acceptLanguageHeader
|
||||||
|
.Split(',')
|
||||||
|
.Select(lang => lang.Split(';')[0].Trim())
|
||||||
|
.Select(lang => lang.Split('-')[0].ToLower()) // Get only language code (pt from pt-BR)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Find first supported language
|
||||||
|
foreach (var browserLang in browserLanguages)
|
||||||
|
{
|
||||||
|
if (_supportedLanguages.Contains(browserLang))
|
||||||
|
{
|
||||||
|
return browserLang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Services/MarkdownService.cs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
using Markdig;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
|
||||||
|
namespace CarneiroTech.Services;
|
||||||
|
|
||||||
|
public class MarkdownService : IMarkdownService
|
||||||
|
{
|
||||||
|
private readonly MarkdownPipeline _pipeline;
|
||||||
|
|
||||||
|
public MarkdownService()
|
||||||
|
{
|
||||||
|
_pipeline = new MarkdownPipelineBuilder()
|
||||||
|
.UseAdvancedExtensions()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ConvertToHtml(string markdown)
|
||||||
|
{
|
||||||
|
return Markdown.ToHtml(markdown, _pipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, object> ParseFrontMatter(string content, out string bodyContent)
|
||||||
|
{
|
||||||
|
bodyContent = content;
|
||||||
|
var frontMatter = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
if (!content.StartsWith("---"))
|
||||||
|
{
|
||||||
|
return frontMatter;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = content.Split('\n');
|
||||||
|
var yamlLines = new List<string>();
|
||||||
|
var bodyLines = new List<string>();
|
||||||
|
var inFrontMatter = false;
|
||||||
|
var frontMatterEnded = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i];
|
||||||
|
|
||||||
|
if (i == 0 && line.Trim() == "---")
|
||||||
|
{
|
||||||
|
inFrontMatter = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFrontMatter && line.Trim() == "---")
|
||||||
|
{
|
||||||
|
inFrontMatter = false;
|
||||||
|
frontMatterEnded = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFrontMatter)
|
||||||
|
{
|
||||||
|
yamlLines.Add(line);
|
||||||
|
}
|
||||||
|
else if (frontMatterEnded)
|
||||||
|
{
|
||||||
|
bodyLines.Add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (yamlLines.Any())
|
||||||
|
{
|
||||||
|
var yaml = string.Join("\n", yamlLines);
|
||||||
|
var deserializer = new DeserializerBuilder().Build();
|
||||||
|
frontMatter = deserializer.Deserialize<Dictionary<string, object>>(yaml)
|
||||||
|
?? new Dictionary<string, object>();
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyContent = string.Join("\n", bodyLines).Trim();
|
||||||
|
return frontMatter;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Views/Cases/Details.cshtml
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
@model CarneiroTech.Models.CaseModel
|
||||||
|
|
||||||
|
<!-- Case Detail Header -->
|
||||||
|
<section class="page-section" style="padding-top: 150px;">
|
||||||
|
<div class="container">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<h1 class="display-4 mb-4">@Model.Metadata.Title</h1>
|
||||||
|
<p class="lead text-muted mb-4">@Model.Metadata.Summary</p>
|
||||||
|
|
||||||
|
<!-- Meta Information -->
|
||||||
|
<div class="row justify-content-center mb-4">
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Metadata.Client))
|
||||||
|
{
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<strong>Cliente:</strong> @Model.Metadata.Client
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Metadata.Industry))
|
||||||
|
{
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<strong>Indústria:</strong> @Model.Metadata.Industry
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Metadata.Timeline))
|
||||||
|
{
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<strong>Timeline:</strong> @Model.Metadata.Timeline
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrEmpty(Model.Metadata.Role))
|
||||||
|
{
|
||||||
|
<div class="col-md-3 mb-2">
|
||||||
|
<strong>Meu Role:</strong> @Model.Metadata.Role
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
@if (Model.Metadata.Tags.Any())
|
||||||
|
{
|
||||||
|
<div class="tags mt-3 mb-4">
|
||||||
|
@foreach (var tag in Model.Metadata.Tags)
|
||||||
|
{
|
||||||
|
<a href="/cases?tag=@tag" class="badge bg-primary me-2 mb-2 text-decoration-none" style="font-size: 0.9rem; padding: 0.5rem 1rem;">@tag</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 mx-auto">
|
||||||
|
<div class="case-content">
|
||||||
|
@Html.Raw(Model.ContentHtml)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<a href="/cases" class="btn btn-outline-primary btn-lg">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Ver Todos os Cases
|
||||||
|
</a>
|
||||||
|
<a href="/#contact" class="btn btn-primary btn-lg ms-2">
|
||||||
|
<i class="fas fa-envelope me-2"></i>Vamos Conversar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<!-- Syntax highlighting for code blocks (optional) -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script>hljs.highlightAll();</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.case-content {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content h2 {
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid #C42127;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content h3 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content h4 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content p {
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content ul,
|
||||||
|
.case-content ol {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content pre {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-left: 4px solid #C42127;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #e83e8c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content blockquote {
|
||||||
|
border-left: 4px solid #C42127;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 2rem 0;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content table th,
|
||||||
|
.case-content table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content table th {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content hr {
|
||||||
|
margin: 2rem 0;
|
||||||
|
border-top: 2px solid #dee2e6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
91
Views/Cases/Index.cshtml
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
@model List<CarneiroTech.Models.CaseModel>
|
||||||
|
@{
|
||||||
|
var selectedTag = ViewData["SelectedTag"] as string;
|
||||||
|
var allTags = ViewBag.AllTags as List<string> ?? new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Page Header-->
|
||||||
|
<header class="masthead" style="padding: 150px 0 100px;">
|
||||||
|
<div class="container">
|
||||||
|
<div class="masthead-subheading">Portfolio</div>
|
||||||
|
<div class="masthead-heading text-uppercase">Nossos Cases</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Cases List-->
|
||||||
|
<section class="page-section bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<!-- Tags Filter -->
|
||||||
|
@if (allTags.Any())
|
||||||
|
{
|
||||||
|
<div class="row mb-5">
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<h5 class="mb-3">Filtrar por tecnologia:</h5>
|
||||||
|
<div class="btn-group-wrap">
|
||||||
|
<a href="/cases" class="btn @(string.IsNullOrEmpty(selectedTag) ? "btn-primary" : "btn-outline-primary") m-1">Todos</a>
|
||||||
|
@foreach (var tag in allTags)
|
||||||
|
{
|
||||||
|
<a href="/cases?tag=@tag" class="btn @(selectedTag == tag ? "btn-primary" : "btn-outline-primary") m-1">@tag</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Cases Grid -->
|
||||||
|
<div class="row">
|
||||||
|
@if (Model != null && Model.Any())
|
||||||
|
{
|
||||||
|
foreach (var caseItem in Model)
|
||||||
|
{
|
||||||
|
<div class="col-lg-4 col-sm-6 mb-4">
|
||||||
|
<!-- Portfolio item -->
|
||||||
|
<div class="portfolio-item">
|
||||||
|
<a class="portfolio-link" href="/cases/@caseItem.Metadata.Slug">
|
||||||
|
<div class="portfolio-hover">
|
||||||
|
<div class="portfolio-hover-content">
|
||||||
|
<p class="text-white fw-bold px-3">@caseItem.Metadata.Summary</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(caseItem.Metadata.Image))
|
||||||
|
{
|
||||||
|
<img class="img-fluid" src="@caseItem.Metadata.Image" alt="@caseItem.Metadata.Title" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="height: 300px; background: linear-gradient(135deg, #C42127 0%, #8B1E23 100%);"></div>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
<div class="portfolio-caption">
|
||||||
|
<div class="portfolio-caption-heading">@caseItem.Metadata.Title</div>
|
||||||
|
<div class="portfolio-caption-subheading text-muted">@caseItem.Metadata.Industry</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
@foreach (var tag in caseItem.Metadata.Tags.Take(3))
|
||||||
|
{
|
||||||
|
<span class="badge bg-secondary me-1">@tag</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<p class="lead text-muted">
|
||||||
|
@if (!string.IsNullOrEmpty(selectedTag))
|
||||||
|
{
|
||||||
|
<span>Nenhum case encontrado para a tag "@selectedTag".</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Em breve, novos cases serão adicionados.</span>
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<a href="/cases" class="btn btn-primary">Ver Todos os Cases</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
302
Views/Home/Index.cshtml
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
@model List<CarneiroTech.Models.CaseModel>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = ViewData["Title"] ?? "Carneiro Tech - Solution Design & Technical Consulting";
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Masthead-->
|
||||||
|
<header class="masthead">
|
||||||
|
<div class="container">
|
||||||
|
<div class="masthead-subheading">Solution Design & Technical Consulting</div>
|
||||||
|
<div class="masthead-heading text-uppercase">Conectando Negócio e Tecnologia</div>
|
||||||
|
<a class="btn btn-primary btn-xl text-uppercase" href="#services">Saiba Mais</a>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Services-->
|
||||||
|
<section class="page-section" id="services">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="section-heading text-uppercase">Serviços</h2>
|
||||||
|
<h3 class="section-subheading text-muted">20+ anos conectando negócio e tecnologia</h3>
|
||||||
|
</div>
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<span class="fa-stack fa-4x">
|
||||||
|
<i class="fas fa-circle fa-stack-2x text-primary"></i>
|
||||||
|
<i class="fas fa-lightbulb fa-stack-1x fa-inverse"></i>
|
||||||
|
</span>
|
||||||
|
<h4 class="my-3">Solution Design</h4>
|
||||||
|
<p class="text-muted">Desenho de soluções técnicas que conectam objetivos de negócio com arquitetura de sistemas. Experiência com integrações SAP, arquiteturas enterprise e modernização de legados.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<span class="fa-stack fa-4x">
|
||||||
|
<i class="fas fa-circle fa-stack-2x text-primary"></i>
|
||||||
|
<i class="fas fa-clipboard-check fa-stack-1x fa-inverse"></i>
|
||||||
|
</span>
|
||||||
|
<h4 class="my-3">Technical Consulting</h4>
|
||||||
|
<p class="text-muted">Assessoria técnica para tomada de decisão: definição de MVP, priorização de backlog, análise de viabilidade técnica e due diligence de produtos digitais.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<span class="fa-stack fa-4x">
|
||||||
|
<i class="fas fa-circle fa-stack-2x text-primary"></i>
|
||||||
|
<i class="fas fa-code fa-stack-1x fa-inverse"></i>
|
||||||
|
</span>
|
||||||
|
<h4 class="my-3">Technical Proposals</h4>
|
||||||
|
<p class="text-muted">Elaboração de propostas técnicas detalhadas: estimativas, arquitetura, tecnologias, riscos e roadmap de implementação para projetos complexos.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Portfolio Grid (Cases)-->
|
||||||
|
<section class="page-section bg-light" id="portfolio">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="section-heading text-uppercase">Cases</h2>
|
||||||
|
<h3 class="section-subheading text-muted">Projetos que transformaram negócios</h3>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
@if (Model != null && Model.Any())
|
||||||
|
{
|
||||||
|
foreach (var caseItem in Model.Take(6))
|
||||||
|
{
|
||||||
|
<div class="col-lg-4 col-sm-6 mb-4">
|
||||||
|
<!-- Portfolio item -->
|
||||||
|
<div class="portfolio-item">
|
||||||
|
<a class="portfolio-link" href="/cases/@caseItem.Metadata.Slug">
|
||||||
|
<div class="portfolio-hover">
|
||||||
|
<div class="portfolio-hover-content">
|
||||||
|
<p class="text-white fw-bold">@caseItem.Metadata.Summary</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrEmpty(caseItem.Metadata.Image))
|
||||||
|
{
|
||||||
|
<img class="img-fluid" src="@caseItem.Metadata.Image" alt="@caseItem.Metadata.Title" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div style="height: 300px; background: linear-gradient(135deg, #C42127 0%, #8B1E23 100%);"></div>
|
||||||
|
}
|
||||||
|
</a>
|
||||||
|
<div class="portfolio-caption">
|
||||||
|
<div class="portfolio-caption-heading">@caseItem.Metadata.Title</div>
|
||||||
|
<div class="portfolio-caption-subheading text-muted">@caseItem.Metadata.Industry</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="col-12 text-center">
|
||||||
|
<p class="text-muted">Em breve, novos cases serão adicionados.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<a class="btn btn-primary btn-xl text-uppercase" href="/cases">Ver Todos os Cases</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- About-->
|
||||||
|
<section class="page-section" id="about">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="section-heading text-uppercase">Sobre</h2>
|
||||||
|
<h3 class="section-subheading text-muted">Ricardo Carneiro - Carneiro Tech</h3>
|
||||||
|
</div>
|
||||||
|
<ul class="timeline">
|
||||||
|
<li>
|
||||||
|
<div class="timeline-image"><i class="fas fa-graduation-cap fa-3x text-white"></i></div>
|
||||||
|
<div class="timeline-panel">
|
||||||
|
<div class="timeline-heading">
|
||||||
|
<h4>2000-2005</h4>
|
||||||
|
<h4 class="subheading">Início da Jornada</h4>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body"><p class="text-muted">Primeiros passos na tecnologia: desenvolvimento web, banco de dados e sistemas corporativos. Formação sólida em Engenharia de Software.</p></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="timeline-inverted">
|
||||||
|
<div class="timeline-image"><i class="fas fa-building fa-3x text-white"></i></div>
|
||||||
|
<div class="timeline-panel">
|
||||||
|
<div class="timeline-heading">
|
||||||
|
<h4>2005-2015</h4>
|
||||||
|
<h4 class="subheading">Enterprise & SAP</h4>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body"><p class="text-muted">Especialização em integrações SAP e arquiteturas enterprise. Projetos em multinacionais nos setores healthcare, varejo e manufatura.</p></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div class="timeline-image"><i class="fas fa-rocket fa-3x text-white"></i></div>
|
||||||
|
<div class="timeline-panel">
|
||||||
|
<div class="timeline-heading">
|
||||||
|
<h4>2015-2020</h4>
|
||||||
|
<h4 class="subheading">Digital Transformation</h4>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body"><p class="text-muted">Liderança técnica em transformação digital: cloud migration, modernização de legados e implementação de metodologias ágeis.</p></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="timeline-inverted">
|
||||||
|
<div class="timeline-image"><i class="fas fa-handshake fa-3x text-white"></i></div>
|
||||||
|
<div class="timeline-panel">
|
||||||
|
<div class="timeline-heading">
|
||||||
|
<h4>2020-Hoje</h4>
|
||||||
|
<h4 class="subheading">Consultoria Independente</h4>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-body"><p class="text-muted">Fundação da Carneiro Tech: consultoria especializada em Solution Design, Technical Proposals e Due Diligence para empresas de diversos portes.</p></div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li class="timeline-inverted">
|
||||||
|
<div class="timeline-image">
|
||||||
|
<h4 style="vertical-align:central">
|
||||||
|
Vamos
|
||||||
|
<br />
|
||||||
|
Trabalhar
|
||||||
|
<br />
|
||||||
|
Juntos!
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Contact-->
|
||||||
|
<section class="page-section" id="contact">
|
||||||
|
<div class="container">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2 class="section-heading text-uppercase">Contato</h2>
|
||||||
|
<h3 class="section-subheading text-white">Vamos conversar sobre seu desafio técnico</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert messages -->
|
||||||
|
<div id="contactAlert" class="alert text-center" style="display: none;"></div>
|
||||||
|
|
||||||
|
<form id="contactForm">
|
||||||
|
<div class="row align-items-stretch mb-5">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<input class="form-control" id="contactName" name="name" type="text" placeholder="Seu Nome *" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<input class="form-control" id="contactEmail" name="email" type="email" placeholder="Seu Email *" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-md-0">
|
||||||
|
<input class="form-control" id="contactPhone" name="phone" type="tel" placeholder="Seu Telefone" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group form-group-textarea mb-md-0">
|
||||||
|
<textarea class="form-control" id="contactMessage" name="message" placeholder="Sua Mensagem *" style="height: 100%;" required></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<button class="btn btn-primary btn-xl text-uppercase" id="submitButton" type="submit">
|
||||||
|
<span id="buttonText">Enviar Mensagem</span>
|
||||||
|
<span id="buttonSpinner" class="spinner-border spinner-border-sm ms-2" style="display: none;" role="status" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- WhatsApp Contact Option -->
|
||||||
|
<div class="text-center mt-5">
|
||||||
|
<div class="mb-3">
|
||||||
|
<span class="text-white-50" style="font-size: 1.1rem;">OU</span>
|
||||||
|
</div>
|
||||||
|
<a href="https://wa.me/5511970792602?text=%5BSite%5D%20Ol%C3%A1%21%20Eu%20gostaria%20de%20conversar%20com%20voc%C3%AA%20sobre%20uma%20poss%C3%ADvel%20proposta%20comercial."
|
||||||
|
class="btn btn-success btn-xl text-uppercase"
|
||||||
|
target="_blank"
|
||||||
|
style="background-color: #25D366; border-color: #25D366;">
|
||||||
|
<i class="fab fa-whatsapp me-2"></i>
|
||||||
|
Falar via WhatsApp
|
||||||
|
</a>
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-white-50">Resposta rápida e direta</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('contactForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const form = this;
|
||||||
|
const submitButton = document.getElementById('submitButton');
|
||||||
|
const buttonText = document.getElementById('buttonText');
|
||||||
|
const buttonSpinner = document.getElementById('buttonSpinner');
|
||||||
|
const alertDiv = document.getElementById('contactAlert');
|
||||||
|
|
||||||
|
// Get form data
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {
|
||||||
|
name: formData.get('name'),
|
||||||
|
email: formData.get('email'),
|
||||||
|
phone: formData.get('phone') || '',
|
||||||
|
message: formData.get('message')
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Sending form data:', data);
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
submitButton.disabled = true;
|
||||||
|
buttonText.textContent = 'Enviando...';
|
||||||
|
buttonSpinner.style.display = 'inline-block';
|
||||||
|
alertDiv.style.display = 'none';
|
||||||
|
|
||||||
|
// Create form and submit via POST (Google Apps Script accepts form data)
|
||||||
|
const scriptUrl = 'https://script.google.com/macros/s/AKfycbwYIzI1TGYEKYmkUhhdvNDQcrpInwNJ9Olk24KLZYEb4AycMaGao2qbfk2gmPsp9yUZ4A/exec';
|
||||||
|
|
||||||
|
// Create a hidden iframe to submit the form
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
iframe.style.display = 'none';
|
||||||
|
iframe.name = 'contact-frame';
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
// Create a temporary form
|
||||||
|
const tempForm = document.createElement('form');
|
||||||
|
tempForm.method = 'POST';
|
||||||
|
tempForm.action = scriptUrl;
|
||||||
|
tempForm.target = 'contact-frame';
|
||||||
|
|
||||||
|
// Add form fields
|
||||||
|
Object.keys(data).forEach(key => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = key;
|
||||||
|
input.value = data[key];
|
||||||
|
tempForm.appendChild(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add to document and submit
|
||||||
|
document.body.appendChild(tempForm);
|
||||||
|
|
||||||
|
// Set timeout to show success message
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.log('Form submitted successfully (timeout)');
|
||||||
|
|
||||||
|
// Success - assume it worked after 2 seconds
|
||||||
|
alertDiv.className = 'alert alert-success text-center';
|
||||||
|
alertDiv.textContent = 'Mensagem enviada com sucesso! Entraremos em contato em breve.';
|
||||||
|
alertDiv.style.display = 'block';
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
// Scroll to alert
|
||||||
|
alertDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
|
||||||
|
// Reset button state
|
||||||
|
submitButton.disabled = false;
|
||||||
|
buttonText.textContent = 'Enviar Mensagem';
|
||||||
|
buttonSpinner.style.display = 'none';
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
document.body.removeChild(tempForm);
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
tempForm.submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
6
Views/Home/Privacy.cshtml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "Privacy Policy";
|
||||||
|
}
|
||||||
|
<h1>@ViewData["Title"]</h1>
|
||||||
|
|
||||||
|
<p>Use this page to detail your site's privacy policy.</p>
|
||||||
25
Views/Shared/Error.cshtml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
@model ErrorViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Error";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (Model.ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
156
Views/Shared/_Layout.cshtml
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
|
|
||||||
|
<!-- SEO Meta Tags -->
|
||||||
|
<title>@ViewData["Title"]</title>
|
||||||
|
<meta name="description" content="@ViewData["Description"]" />
|
||||||
|
<meta name="keywords" content="@ViewData["Keywords"]" />
|
||||||
|
<meta name="author" content="Ricardo Carneiro - Carneiro Tech" />
|
||||||
|
<link rel="canonical" href="@($"https://carneirotech.com{Context.Request.Path}")" />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="@($"https://carneirotech.com{Context.Request.Path}")" />
|
||||||
|
<meta property="og:title" content="@ViewData["Title"]" />
|
||||||
|
<meta property="og:description" content="@ViewData["Description"]" />
|
||||||
|
<meta property="og:image" content="@(ViewData["OgImage"] ?? "https://carneirotech.com/img/logo.svg")" />
|
||||||
|
<meta property="og:locale" content="pt_BR" />
|
||||||
|
|
||||||
|
<!-- Twitter Card -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="@ViewData["Title"]" />
|
||||||
|
<meta name="twitter:description" content="@ViewData["Description"]" />
|
||||||
|
<meta name="twitter:image" content="@(ViewData["OgImage"] ?? "https://carneirotech.com/img/logo.svg")" />
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" type="image/x-icon" href="~/favicon.ico" />
|
||||||
|
|
||||||
|
<!-- Font Awesome icons (free version)-->
|
||||||
|
<script src="https://use.fontawesome.com/releases/v6.3.0/js/all.js" crossorigin="anonymous"></script>
|
||||||
|
<!-- Google fonts-->
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto+Slab:400,100,300,700" rel="stylesheet" type="text/css" />
|
||||||
|
<!-- Core theme CSS (includes Bootstrap)-->
|
||||||
|
<link href="~/css/styles.css" rel="stylesheet" />
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link href="~/css/custom.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
<!-- JSON-LD Structured Data -->
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@@context": "https://schema.org",
|
||||||
|
"@@type": "ProfessionalService",
|
||||||
|
"name": "Carneiro Tech",
|
||||||
|
"description": "Solution Design & Technical Consulting",
|
||||||
|
"url": "https://carneirotech.com",
|
||||||
|
"logo": "https://carneirotech.com/img/logo.svg",
|
||||||
|
"image": "https://carneirotech.com/img/logo.svg",
|
||||||
|
"priceRange": "$$",
|
||||||
|
"address": {
|
||||||
|
"@@type": "PostalAddress",
|
||||||
|
"addressLocality": "São Bernardo do Campo",
|
||||||
|
"addressRegion": "SP",
|
||||||
|
"addressCountry": "BR"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://linkedin.com/in/ricardo-carneiro"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Microsoft Clarity -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function(c,l,a,r,i,t,y){
|
||||||
|
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
||||||
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
||||||
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
||||||
|
})(window, document, "clarity", "script", "up7yoisy52");
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body id="page-top">
|
||||||
|
<!-- Navigation-->
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light fixed-top" id="mainNav">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<img src="~/img/LogoPequeno.png" alt="Carneiro Tech" style="height: 45px;" />
|
||||||
|
<span style="color: #8B1E23">Carneiro Tech</span>
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
Menu
|
||||||
|
<i class="fas fa-bars ms-1"></i>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarResponsive">
|
||||||
|
<ul class="navbar-nav text-uppercase ms-auto py-4 py-lg-0">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/#services">Serviços</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/cases">Cases</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/#about">Sobre</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/#contact">Contato</a></li>
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-globe"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="languageDropdown">
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="changeLanguage('pt'); return false;">🇧🇷 Português</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="changeLanguage('en'); return false;">🇺🇸 English</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="changeLanguage('es'); return false;">🇪🇸 Español</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
@RenderBody()
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer py-4">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-4 text-lg-start">Copyright © @DateTime.Now.Year Carneiro Tech - Ricardo Carneiro</div>
|
||||||
|
<div class="col-lg-4 my-3 my-lg-0">
|
||||||
|
<a class="btn btn-dark btn-social mx-2" href="https://linkedin.com/in/ricardo-carneiro" aria-label="LinkedIn" target="_blank"><i class="fab fa-linkedin-in"></i></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4 text-lg-end">
|
||||||
|
<a class="link-dark text-decoration-none me-3" asp-controller="Home" asp-action="Privacy">Privacy Policy</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap core JS-->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- Core theme JS-->
|
||||||
|
<script src="~/js/scripts.js"></script>
|
||||||
|
|
||||||
|
<!-- Language Switcher -->
|
||||||
|
<script>
|
||||||
|
function changeLanguage(lang) {
|
||||||
|
// Create form to submit language change
|
||||||
|
const form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = '/Language/SetLanguage';
|
||||||
|
|
||||||
|
const langInput = document.createElement('input');
|
||||||
|
langInput.type = 'hidden';
|
||||||
|
langInput.name = 'language';
|
||||||
|
langInput.value = lang;
|
||||||
|
form.appendChild(langInput);
|
||||||
|
|
||||||
|
const returnUrlInput = document.createElement('input');
|
||||||
|
returnUrlInput.type = 'hidden';
|
||||||
|
returnUrlInput.name = 'returnUrl';
|
||||||
|
returnUrlInput.value = window.location.pathname + window.location.search + window.location.hash;
|
||||||
|
form.appendChild(returnUrlInput);
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@await RenderSectionAsync("Scripts", required: false)
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
Views/Shared/_Layout.cshtml.css
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||||
|
for details on configuring this project to bundle and minify static web assets. */
|
||||||
|
|
||||||
|
a.navbar-brand {
|
||||||
|
white-space: normal;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0077cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #1b6ec2;
|
||||||
|
border-color: #1861ac;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-top {
|
||||||
|
border-top: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
.border-bottom {
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow {
|
||||||
|
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.accept-policy {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 60px;
|
||||||
|
}
|
||||||
2
Views/Shared/_ValidationScriptsPartial.cshtml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
|
||||||
|
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
|
||||||
3
Views/_ViewImports.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@using CarneiroTech
|
||||||
|
@using CarneiroTech.Models
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
3
Views/_ViewStart.cshtml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
8
appsettings.Development.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
appsettings.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
52
deploy-to-oci.sh
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Local Deploy Script - CarneiroTech to OCI
|
||||||
|
# Usage: ./deploy-to-oci.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SERVER="ubuntu@129.146.116.218"
|
||||||
|
APP_DIR="/home/ubuntu/apps/carneirotech"
|
||||||
|
LOCAL_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
echo "🚀 CarneiroTech - Deploy to OCI"
|
||||||
|
echo "================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Create tarball excluding unnecessary files
|
||||||
|
echo "📦 Creating deployment package..."
|
||||||
|
cd "$LOCAL_DIR"
|
||||||
|
tar --exclude='bin' \
|
||||||
|
--exclude='obj' \
|
||||||
|
--exclude='.vs' \
|
||||||
|
--exclude='*.user' \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='deploy-to-oci.sh' \
|
||||||
|
-czf /tmp/carneirotech-deploy.tar.gz .
|
||||||
|
|
||||||
|
echo "✅ Package created: $(du -h /tmp/carneirotech-deploy.tar.gz | cut -f1)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Transfer to server
|
||||||
|
echo "📤 Transferring to server..."
|
||||||
|
scp /tmp/carneirotech-deploy.tar.gz "$SERVER:$APP_DIR/"
|
||||||
|
rm /tmp/carneirotech-deploy.tar.gz
|
||||||
|
|
||||||
|
# Extract and deploy on server
|
||||||
|
echo "📥 Extracting on server..."
|
||||||
|
ssh "$SERVER" << 'ENDSSH'
|
||||||
|
cd /home/ubuntu/apps/carneirotech
|
||||||
|
tar -xzf carneirotech-deploy.tar.gz
|
||||||
|
rm carneirotech-deploy.tar.gz
|
||||||
|
echo "✅ Files extracted"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run deploy script
|
||||||
|
echo "🚀 Running deployment..."
|
||||||
|
./deploy.sh
|
||||||
|
ENDSSH
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✨ Deployment completed!"
|
||||||
|
echo "🌐 Check your site at: https://carneirotech.com"
|
||||||
26
docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
carneirotech-web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: carneirotech-web
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:5008:5008"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
- ASPNETCORE_URLS=http://+:5008
|
||||||
|
networks:
|
||||||
|
- carneirotech-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5008/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
carneirotech-network:
|
||||||
|
driver: bridge
|
||||||
BIN
wwwroot/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
wwwroot/assets/img/about/1.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
wwwroot/assets/img/about/2.jpg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
wwwroot/assets/img/about/3.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
wwwroot/assets/img/about/4.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
1
wwwroot/assets/img/close-icon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 579.74 579.74"><defs><style>.cls-1{fill:none;stroke:#000;stroke-miterlimit:10;stroke-width:6px;}</style></defs><line class="cls-1" x1="2.12" y1="2.12" x2="577.62" y2="577.62"/><line class="cls-1" x1="2.12" y1="577.62" x2="577.62" y2="2.12"/></svg>
|
||||||
|
After Width: | Height: | Size: 333 B |
BIN
wwwroot/assets/img/header-bg.jpg
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
wwwroot/assets/img/header-bg_old.jpg
Normal file
|
After Width: | Height: | Size: 233 KiB |
34
wwwroot/assets/img/logos/facebook.svg
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 2030.6 546.3" style="enable-background:new 0 0 2030.6 546.3;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#CED4DA;}
|
||||||
|
</style>
|
||||||
|
<g>
|
||||||
|
<path class="st0" d="M1045.4,269.1c-16.8,0-28.9,5.5-41.1,11.1v126.7c11.7,1.1,18.5,1.1,29.6,1.1c40.2,0,45.8-18.4,45.8-44.2v-60.5
|
||||||
|
C1079.7,284.3,1073.4,269.1,1045.4,269.1L1045.4,269.1z M778.1,262.2c-27.9,0-34.3,15.3-34.3,34.3v10.7h68.6v-10.7
|
||||||
|
C812.4,277.5,806,262.2,778.1,262.2z M260.4,394.1c0,15,7.1,22.8,22.7,22.8c16.8,0,26.7-5.5,39-11.1v-30.1h-36.7
|
||||||
|
C268,375.7,260.4,379,260.4,394.1L260.4,394.1z M1305.4,269.1c-28,0-37.7,15.3-37.7,34.3v69.4c0,19.1,9.7,34.4,37.7,34.4
|
||||||
|
c27.9,0,37.7-15.3,37.7-34.4v-69.4C1343,284.3,1333.3,269.1,1305.4,269.1z M123.3,471.8H41.1v-199H0v-68.6h41.1v-41.2
|
||||||
|
c0-55.9,23.2-89.2,89.1-89.2H185v68.6h-34.3c-25.7,0-27.3,9.6-27.3,27.5l-0.1,34.3h62.1l-7.3,68.6h-54.9L123.3,471.8L123.3,471.8z
|
||||||
|
M404.3,472.4h-68.5l-3-17.3c-31.3,17.3-59.2,20.1-77.6,20.1c-50.3,0-77-33.6-77-80c0-54.8,31.2-74.3,87-74.3H322V309
|
||||||
|
c0-28-3.2-36.2-46.2-36.2h-70.3l6.9-68.6h76.8c94.3,0,115,29.8,115,105.3L404.3,472.4L404.3,472.4z M637.3,277.9
|
||||||
|
c-42.6-7.3-54.9-8.9-75.4-8.9c-36.9,0-48,8.1-48,39.4v59.2c0,31.3,11.1,39.5,48,39.5c20.5,0,32.8-1.6,75.4-9V465
|
||||||
|
c-37.3,8.4-61.7,10.6-82.2,10.6c-88.3,0-123.4-46.4-123.4-113.5v-48c0-67.1,35.1-113.6,123.4-113.6c20.6,0,44.9,2.2,82.2,10.6
|
||||||
|
V277.9L637.3,277.9z M894.6,362H743.8v5.5c0,31.3,11.1,39.5,48,39.5c33.1,0,53.3-1.6,95.9-9V465c-41.1,8.4-62.4,10.6-102.7,10.6
|
||||||
|
c-88.3,0-123.4-46.4-123.4-113.5v-54.9c0-58.7,26-106.7,116.5-106.7s116.5,47.5,116.5,106.7V362z M1161.9,363.3
|
||||||
|
c0,64.8-18.5,112.1-130.7,112.1c-40.5,0-64.3-3.6-109-10.4V94.5l82.2-13.7v129.6c17.8-6.6,40.8-10,61.7-10
|
||||||
|
c82.2,0,95.9,36.9,95.9,96.1L1161.9,363.3L1161.9,363.3z M1425.3,364.7c0,55.9-23.1,110.1-119.7,110.1
|
||||||
|
c-96.6,0-120.1-54.2-120.1-110.1v-54c0-55.9,23.5-110.2,120.1-110.2c96.6,0,119.7,54.2,119.7,110.2V364.7L1425.3,364.7z
|
||||||
|
M1688.6,364.7c0,55.9-23.1,110.1-119.7,110.1c-96.6,0-120.1-54.2-120.1-110.1v-54c0-55.9,23.5-110.2,120.1-110.2
|
||||||
|
c96.6,0,119.7,54.2,119.7,110.2V364.7L1688.6,364.7z M1958.8,471.8h-89.1l-75.3-125.8v125.8h-82.2V94.5l82.2-13.7v242.9l75.3-119.4
|
||||||
|
h89.1l-82.3,130.3L1958.8,471.8z M1568.7,269.1c-27.9,0-37.6,15.3-37.6,34.3v69.4c0,19.1,9.7,34.4,37.6,34.4
|
||||||
|
c27.9,0,37.8-15.3,37.8-34.4v-69.4C1606.4,284.3,1596.6,269.1,1568.7,269.1L1568.7,269.1z M2005.7,424.9
|
||||||
|
c13.8,0,24.9,11.3,24.9,25.4c0,14.3-11,25.5-25,25.5c-13.9,0-25.1-11.2-25.1-25.5c0-14.1,11.3-25.4,25.1-25.4H2005.7z
|
||||||
|
M2005.6,428.9c-11.2,0-20.3,9.6-20.3,21.4c0,12.1,9.1,21.5,20.4,21.5c11.3,0.1,20.3-9.5,20.3-21.4c0-11.9-9-21.6-20.3-21.6H2005.6
|
||||||
|
z M2000.9,465.1h-4.5v-28.3c2.4-0.3,4.6-0.7,8-0.7c4.3,0,7.1,0.9,8.8,2.1c1.7,1.3,2.6,3.2,2.6,5.9c0,3.7-2.5,6-5.5,6.9v0.2
|
||||||
|
c2.5,0.5,4.2,2.7,4.7,6.9c0.7,4.4,1.3,6.1,1.8,7h-4.7c-0.7-0.9-1.4-3.5-1.9-7.2c-0.7-3.6-2.5-5-6.1-5h-3.1L2000.9,465.1
|
||||||
|
L2000.9,465.1z M2000.9,449.4h3.3c3.7,0,6.9-1.4,6.9-4.9c0-2.5-1.8-5-6.9-5c-1.5,0-2.5,0.1-3.3,0.2V449.4L2000.9,449.4z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
35
wwwroot/assets/img/logos/google.svg
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 2500 928" style="enable-background:new 0 0 2500 928;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#CED4DA;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M307.9,112.1h22.2c77.2,1.7,153.1,32.7,207.6,87.7c-20.1,20.6-40.7,40.3-60.4,60.8
|
||||||
|
c-30.6-27.7-67.5-49.1-107.8-56.6c-59.6-12.6-123.7-1.3-173.7,32.7c-54.5,35.7-91.4,96.1-99.4,160.7c-8.8,63.8,9.2,130.9,50.8,180.4
|
||||||
|
c39.8,48.2,100.7,78.4,163.6,80.5c58.7,3.4,120-14.7,162.8-55.8c33.6-28.9,49.1-73,54.1-115.8c-69.6,0-139.3,0.4-208.9,0v-86.4H612
|
||||||
|
c15.1,92.7-6.7,197.1-77.2,263.4c-47,47-112,74.7-178.3,80.1c-64.2,6.3-130.5-5.9-187.5-36.9c-68.4-36.5-122.9-98.2-149.7-170.7
|
||||||
|
C-5.9,469.5-6.3,394,17.2,326.8c21.4-61.2,62.5-115.4,115.4-153.1C183.3,136.4,245,115.8,307.9,112.1z"/>
|
||||||
|
<path class="st0" d="M1989.9,133.9h89.8v599c-29.8,0-60,0.4-89.8-0.4C1990.4,533.2,1989.9,333.5,1989.9,133.9L1989.9,133.9z"/>
|
||||||
|
<path class="st0" d="M811.7,341.5C867,331,927,342.8,972.7,375.9c41.5,29.4,70.5,75.5,79.3,125.8c11.3,58.3-2.9,122.1-40.7,168.2
|
||||||
|
c-40.7,51.6-107.4,79.3-172.4,75.1c-59.6-3.4-117.4-33.1-152.7-81.8c-39.8-53.7-49.5-127.5-27.7-190.4
|
||||||
|
C680.4,405.3,742,353.7,811.7,341.5 M824.2,421.2c-22.7,5.9-43.6,18.9-58.7,37.3c-40.7,48.7-38.2,127.9,6.7,173.2
|
||||||
|
c25.6,26,64.2,38.2,99.8,31c33.1-5.9,62.1-28.1,78-57.5c27.7-49.9,19.7-118.7-22.7-157.7C900.2,422.5,860.3,412,824.2,421.2
|
||||||
|
L824.2,421.2z"/>
|
||||||
|
<path class="st0" d="M1256.3,341.5c63.3-12.2,132.6,5.5,179.9,49.9c77.2,69.2,85.6,198.8,19.7,278.5c-39.8,50.3-104.4,78-168.2,75.1
|
||||||
|
c-60.8-1.7-120.8-31.9-156.9-81.8c-40.7-54.9-49.5-130.5-26.4-194.6C1127.5,403.2,1187.9,353.3,1256.3,341.5 M1268.9,421.2
|
||||||
|
c-22.7,5.9-43.6,18.9-58.7,36.9c-40.3,47.8-38.6,125.8,4.6,171.6c25.6,27.3,65.4,40.7,102.3,33.1c32.7-6.3,62.1-28.1,78-57.5
|
||||||
|
c27.3-50.3,19.3-119.1-23.5-158.1C1344.4,422.1,1304.5,412,1268.9,421.2L1268.9,421.2z"/>
|
||||||
|
<path class="st0" d="M1633.4,365.8c48.2-30.2,112.4-38.6,164.4-12.6c16.4,7.1,29.8,19.3,42.8,31.5c0.4-11.3,0-23.1,0.4-34.8
|
||||||
|
c28.1,0.4,56.2,0,84.7,0.4v370c-0.4,55.8-14.7,114.9-54.9,155.6c-44,44.9-111.6,58.7-172.4,49.5c-65-9.6-121.6-57-146.8-117
|
||||||
|
c25.2-12.2,51.6-21.8,77.6-33.1c14.7,34.4,44.5,63.8,81.8,70.5c37.3,6.7,80.5-2.5,104.9-33.6c26-31.9,26-75.5,24.7-114.5
|
||||||
|
c-19.3,18.9-41.5,35.7-68.4,41.9c-58.3,16.4-122.5-3.8-167.4-43.2c-45.3-39.4-72.1-100.3-69.6-160.7
|
||||||
|
C1536.5,467.4,1575.1,401.5,1633.4,365.8 M1720.2,419.5c-25.6,4.2-49.5,18.5-65.9,38.2c-39.4,47-39.4,122.1,0.4,168.2
|
||||||
|
c22.7,27.3,59.1,42.4,94.4,38.6c33.1-3.4,63.8-24.3,80.1-53.3c27.7-49.1,23.1-115.8-14.3-158.6
|
||||||
|
C1791.9,426.2,1755,413.2,1720.2,419.5L1720.2,419.5z"/>
|
||||||
|
<path class="st0" d="M2187.5,387.2c50.3-47,127.9-62.9,192.5-38.2c61.2,23.1,100.3,81.4,120,141.4c-91,37.8-181.6,75.1-272.7,112.8
|
||||||
|
c12.6,23.9,31.9,45.7,57.9,54.5c36.5,13,80.1,8.4,110.7-15.9c12.2-9.2,21.8-21.4,31-33.1c23.1,15.5,46.1,30.6,69.2,46.1
|
||||||
|
c-32.7,49.1-87.7,83.5-146.8,88.9c-65.4,8-135.1-17.2-177.4-68.4C2102.3,594.9,2109.1,459.8,2187.5,387.2 M2232.4,464.8
|
||||||
|
c-14.3,20.6-20.1,45.7-19.7,70.5c60.8-25.2,121.6-50.3,182.5-75.9c-10.1-23.5-34.4-37.8-59.1-41.5
|
||||||
|
C2296.1,410.7,2254.6,432.1,2232.4,464.8z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
24
wwwroot/assets/img/logos/ibm.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 2500 1000" style="enable-background:new 0 0 2500 1000;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#CED4DA;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M0,0v68.4h486.6V0H0z M555.6,0v68.4H1249c0,0-70.8-68.4-164.6-68.4H555.6z M1385.1,0v68.4h419.5L1779.7,0
|
||||||
|
H1385.1z M2105.4,0l-24.9,68.4h415.7V0H2105.4z M0,133.1v68.4h486.6v-68.4H0z M555.6,133.2v68.3h773.9c0,0-9-52.7-24.8-68.3
|
||||||
|
L555.6,133.2L555.6,133.2z M1385.1,133.2v68.3h465.5l-23-68.3L1385.1,133.2L1385.1,133.2z M2055.6,133.2l-23,68.3h463.7v-68.3
|
||||||
|
H2055.6z M139.8,266.1v68.5h210.7v-68.5L139.8,266.1L139.8,266.1z M695.4,266.1v68.5h210.7v-68.5L695.4,266.1L695.4,266.1z
|
||||||
|
M1111.1,266.1v68.5h210.7c0,0,13.4-36.2,13.4-68.5L1111.1,266.1L1111.1,266.1z M1524.9,266.1v68.5h373.6l-24.9-68.5L1524.9,266.1
|
||||||
|
L1524.9,266.1z M2009.7,266.1l-25,68.5h375.5v-68.5L2009.7,266.1L2009.7,266.1z M139.8,399.3v68.4h210.7v-68.4H139.8L139.8,399.3z
|
||||||
|
M695.4,399.3v68.4h538.3c0,0,45-35.1,59.4-68.4H695.4z M1524.9,399.3v68.4h210.7v-38.1l13.4,38.1h386l14.4-38.1v38.1h210.7v-68.4
|
||||||
|
h-395.6l-21,57.9l-21.1-57.9H1524.9z M139.8,532.3v68.4h210.7v-68.4H139.8z M695.4,532.3v68.4h597.7c-14.3-33.2-59.4-68.4-59.4-68.4
|
||||||
|
H695.4z M1524.9,532.3v68.4h210.7v-68.4H1524.9z M1773.9,532.3l25.5,68.4h289.5l24.2-68.4H1773.9z M2149.4,532.3v68.4h210.7v-68.4
|
||||||
|
H2149.4z M139.8,665.4v68.4h210.7v-68.4H139.8z M695.4,665.4v68.4h210.7v-68.4H695.4z M1111.1,665.4v68.4h224.1
|
||||||
|
c0-32.3-13.4-68.4-13.4-68.4H1111.1L1111.1,665.4z M1524.9,665.4v68.4h210.7v-68.4H1524.9z M1821.8,665.4l24.7,68.4h194l24.9-68.4
|
||||||
|
H1821.8z M2149.4,665.4v68.4h210.7v-68.4H2149.4z M3.8,798.4v68.5h486.6v-68.5H3.8z M555.6,798.4v68.5h749.1
|
||||||
|
c15.8-15.7,24.8-68.5,24.8-68.5H555.6L555.6,798.4z M1388.9,798.4v68.5h346.8v-68.5H1388.9z M1869.7,798.4l25.4,68.5h98.7l23.8-68.5
|
||||||
|
H1869.7z M2149.4,798.4v68.5H2500v-68.5H2149.4z M3.8,931.6v68.4h486.6v-68.4H3.8z M555.6,931.6v68.3h528.8
|
||||||
|
c93.8,0,164.6-68.3,164.6-68.3H555.6z M1388.9,931.6v68.4h346.8v-68.4H1388.9z M1917.9,931.6l24.4,68.2l4.2,0.1l24.8-68.3H1917.9z
|
||||||
|
M2149.4,931.6v68.4H2500v-68.4H2149.4z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
42
wwwroot/assets/img/logos/microsoft.svg
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 2500 534" style="enable-background:new 0 0 2500 534;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#CED4DA;}
|
||||||
|
</style>
|
||||||
|
<path class="st0" d="M2500,241.6v-44h-54.6v-68.4l-1.8,0.6l-51.3,15.7l-1,0.3v51.8h-80.9v-28.9c0-13.4,3-23.7,8.9-30.6
|
||||||
|
c5.9-6.8,14.3-10.2,25-10.2c7.7,0,15.7,1.8,23.7,5.4l2,0.9V88l-0.9-0.3c-7.5-2.7-17.7-4.1-30.3-4.1c-15.9,0-30.4,3.5-43,10.3
|
||||||
|
c-12.6,6.9-22.6,16.7-29.5,29.2c-6.9,12.5-10.5,26.9-10.5,42.8v31.7h-38v44h38v185.2h54.6V241.6h80.9v117.7c0,48.5,22.9,73,68,73
|
||||||
|
c7.4,0,15.2-0.9,23.2-2.6c8.1-1.7,13.6-3.5,16.9-5.4l0.7-0.4v-44.4l-2.2,1.5c-3,2-6.7,3.6-11,4.8c-4.3,1.2-8,1.8-10.8,1.8
|
||||||
|
c-10.6,0-18.4-2.8-23.2-8.5c-4.9-5.7-7.4-15.6-7.4-29.4V241.6H2500L2500,241.6z M2095.9,387.7c-19.8,0-35.4-6.6-46.4-19.5
|
||||||
|
c-11.1-13-16.7-31.5-16.7-55.1c0-24.3,5.6-43.3,16.7-56.6c11-13.1,26.5-19.8,46-19.8c18.9,0,34,6.4,44.8,19
|
||||||
|
c10.8,12.6,16.3,31.5,16.3,56.1c0,24.9-5.2,44-15.4,56.8C2131,381.3,2115.8,387.7,2095.9,387.7 M2098.3,192.1
|
||||||
|
c-37.8,0-67.8,11.1-89.2,32.9c-21.4,21.8-32.2,52.1-32.2,89.9c0,35.9,10.6,64.7,31.5,85.8c20.9,21,49.3,31.7,84.5,31.7
|
||||||
|
c36.6,0,66.1-11.2,87.4-33.4c21.4-22.1,32.2-52.1,32.2-89c0-36.4-10.2-65.5-30.2-86.4C2162.1,202.7,2133.9,192.1,2098.3,192.1
|
||||||
|
M1888.8,192.1c-25.7,0-47,6.6-63.2,19.5c-16.3,13-24.6,30.1-24.6,50.8c0,10.8,1.8,20.3,5.3,28.4c3.5,8.1,9,15.3,16.3,21.3
|
||||||
|
c7.2,6,18.4,12.2,33.2,18.6c12.4,5.1,21.7,9.4,27.6,12.9c5.8,3.3,9.8,6.7,12.1,10c2.2,3.2,3.4,7.6,3.4,13
|
||||||
|
c0,15.4-11.5,22.9-35.3,22.9c-8.8,0-18.8-1.8-29.8-5.5c-10.9-3.6-21.2-8.8-30.6-15.5l-2.3-1.6v52.5l0.8,0.4
|
||||||
|
c7.7,3.6,17.5,6.6,28.9,8.9c11.5,2.4,21.9,3.6,30.9,3.6c27.9,0,50.4-6.6,66.8-19.6c16.5-13.1,24.9-30.6,24.9-52.1
|
||||||
|
c0-15.4-4.5-28.7-13.4-39.4c-8.8-10.6-24.1-20.3-45.4-28.9c-17-6.8-27.9-12.5-32.4-16.8c-4.3-4.2-6.5-10.1-6.5-17.7
|
||||||
|
c0-6.7,2.7-12,8.3-16.3c5.6-4.3,13.4-6.6,23.2-6.6c9.1,0,18.4,1.4,27.6,4.3c9.2,2.8,17.4,6.6,24.1,11.2l2.2,1.5v-49.8l-0.9-0.4
|
||||||
|
c-6.3-2.7-14.5-5-24.5-6.8C1905.8,193,1896.7,192.1,1888.8,192.1 M1658.7,387.7c-19.8,0-35.4-6.6-46.4-19.5
|
||||||
|
c-11.1-13-16.7-31.5-16.7-55.1c0-24.3,5.6-43.3,16.7-56.6c11-13.1,26.5-19.8,46-19.8c18.9,0,34,6.4,44.8,19
|
||||||
|
c10.8,12.6,16.3,31.5,16.3,56.1c0,24.9-5.2,44-15.4,56.8C1693.9,381.3,1678.7,387.7,1658.7,387.7 M1661.2,192.1
|
||||||
|
c-37.8,0-67.8,11.1-89.2,32.9c-21.4,21.8-32.2,52.1-32.2,89.9c0,35.9,10.6,64.7,31.5,85.8c20.9,21,49.3,31.7,84.5,31.7
|
||||||
|
c36.6,0,66.1-11.2,87.4-33.4c21.4-22.1,32.2-52.1,32.2-89c0-36.4-10.2-65.5-30.2-86.4C1725,202.7,1696.7,192.1,1661.2,192.1
|
||||||
|
M1456.9,237.3v-39.7h-53.9v229.2h53.9V309.6c0-19.9,4.5-36.3,13.4-48.7c8.8-12.2,20.5-18.4,34.9-18.4c4.9,0,10.3,0.8,16.2,2.4
|
||||||
|
c5.8,1.6,10.1,3.3,12.6,5.1l2.3,1.6v-54.4l-0.9-0.4c-5-2.1-12.1-3.2-21.1-3.2c-13.5,0-25.7,4.4-36.1,12.9
|
||||||
|
c-9.1,7.5-15.7,17.9-20.7,30.7H1456.9z M1306.4,192.1c-24.7,0-46.8,5.3-65.6,15.8c-18.8,10.5-33.3,25.4-43.2,44.5
|
||||||
|
c-9.9,19-14.9,41.1-14.9,65.9c0,21.7,4.8,41.5,14.4,59c9.6,17.5,23.2,31.3,40.3,40.8c17.2,9.5,37,14.3,58.9,14.3
|
||||||
|
c25.6,0,47.5-5.1,65-15.2l0.7-0.4v-49.4l-2.3,1.7c-7.9,5.8-16.8,10.4-26.4,13.7c-9.5,3.3-18.2,5-25.8,5c-21.2,0-38.1-6.6-50.5-19.7
|
||||||
|
c-12.4-13.1-18.6-31.4-18.6-54.5c0-23.2,6.5-42.1,19.4-55.9c12.8-13.8,29.8-20.9,50.6-20.9c17.7,0,35,6,51.3,17.9l2.3,1.6v-52
|
||||||
|
l-0.7-0.4c-6.1-3.4-14.5-6.3-24.9-8.4C1326.2,193.2,1316,192.1,1306.4,192.1 M1145.6,197.6h-53.9v229.2h53.9V197.6L1145.6,197.6z
|
||||||
|
M1119.2,100c-8.9,0-16.6,3-23,9c-6.4,6-9.6,13.6-9.6,22.5c0,8.8,3.2,16.2,9.5,22c6.3,5.8,14,8.8,23.1,8.8c9,0,16.8-3,23.2-8.8
|
||||||
|
c6.4-5.9,9.6-13.3,9.6-22.1c0-8.6-3.2-16.1-9.4-22.2C1136.4,103.1,1128.6,100,1119.2,100 M984.7,180.7v246.1h55V107h-76.1
|
||||||
|
l-96.8,237.5L772.9,107h-79.2v319.8h51.7V180.7h1.8l99.2,246.1h39l97.6-246.1L984.7,180.7L984.7,180.7z"/>
|
||||||
|
<path class="st0" d="M253.6,253.7H0V0.1h253.6V253.7z"/>
|
||||||
|
<path class="st0" d="M533.6,253.7H280V0.1h253.6L533.6,253.7L533.6,253.7z"/>
|
||||||
|
<path class="st0" d="M253.6,533.9H0V280.3h253.6V533.9z"/>
|
||||||
|
<path class="st0" d="M533.6,533.9H280V280.3h253.6L533.6,533.9L533.6,533.9z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
BIN
wwwroot/assets/img/map-image.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
1
wwwroot/assets/img/navbar-logo.svg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
wwwroot/assets/img/portfolio/1.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
wwwroot/assets/img/portfolio/2.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
wwwroot/assets/img/portfolio/3.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
wwwroot/assets/img/portfolio/4.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
wwwroot/assets/img/portfolio/5.jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
wwwroot/assets/img/portfolio/6.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
wwwroot/assets/img/team/1.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
wwwroot/assets/img/team/2.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
wwwroot/assets/img/team/3.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
368
wwwroot/css/custom.css
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
/* Custom styles for Carneiro Tech - RED THEME */
|
||||||
|
|
||||||
|
/* Color Palette inspired by logo:
|
||||||
|
Primary Red: #C42127 (vibrant red from logo)
|
||||||
|
Dark Red: #8B1E23 (darker shade)
|
||||||
|
Light Background: #FAF7F5 (warm beige/cream)
|
||||||
|
Accent: #E63946 (bright red for hover)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-red: #C42127;
|
||||||
|
--dark-red: #8B1E23;
|
||||||
|
--accent-red: #E63946;
|
||||||
|
--light-bg: #FAF7F5;
|
||||||
|
--cream: #FFF8F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Portfolio hover effect with overlay - IMPROVED */
|
||||||
|
.portfolio-item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 30px rgba(196, 33, 39, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-hover {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(196, 33, 39, 0.95); /* Red overlay instead of yellow */
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-item:hover .portfolio-hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-hover-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-hover-content p {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradient backgrounds for cases without images - RED TONES */
|
||||||
|
.case-gradient-1 {
|
||||||
|
background: linear-gradient(135deg, #C42127 0%, #8B1E23 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-gradient-2 {
|
||||||
|
background: linear-gradient(135deg, #E63946 0%, #C42127 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-gradient-3 {
|
||||||
|
background: linear-gradient(135deg, #D62828 0%, #9D0208 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-gradient-4 {
|
||||||
|
background: linear-gradient(135deg, #DC2F02 0%, #9D0208 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-gradient-5 {
|
||||||
|
background: linear-gradient(135deg, #F48C06 0%, #E85D04 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-gradient-6 {
|
||||||
|
background: linear-gradient(135deg, #8B1E23 0%, #5C0F13 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timeline icon styling - RED */
|
||||||
|
.timeline-image {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-image i {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contact section background - RED GRADIENT */
|
||||||
|
#contact {
|
||||||
|
background: linear-gradient(135deg, #C42127 0%, #8B1E23 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced button styles - RED */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:focus,
|
||||||
|
.btn-primary:active {
|
||||||
|
background-color: var(--dark-red);
|
||||||
|
border-color: var(--dark-red);
|
||||||
|
color: #ffffff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(196, 33, 39, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
color: var(--primary-red);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover,
|
||||||
|
.btn-outline-primary:focus,
|
||||||
|
.btn-outline-primary:active {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge styling - RED */
|
||||||
|
.badge.bg-primary {
|
||||||
|
background-color: var(--primary-red) !important;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.bg-secondary {
|
||||||
|
background-color: #6c757d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case detail page enhancements - RED */
|
||||||
|
.case-content a {
|
||||||
|
color: var(--primary-red);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-content a:hover {
|
||||||
|
color: var(--dark-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Masthead - Natural background with RED TEXT */
|
||||||
|
.masthead {
|
||||||
|
position: relative;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle dark overlay for better text contrast - LIGHTER */
|
||||||
|
.masthead::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.25); /* Apenas 25% de escurecimento */
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.masthead .container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LIGHT NEUTRAL TEXT that complements red - professional look */
|
||||||
|
.masthead-subheading {
|
||||||
|
color: #FFE4B5 !important; /* Bege claro dourado - warm neutral */
|
||||||
|
text-shadow:
|
||||||
|
3px 3px 8px rgba(0, 0, 0, 0.9),
|
||||||
|
-1px -1px 3px rgba(0, 0, 0, 0.7),
|
||||||
|
1px 1px 2px rgba(0, 0, 0, 0.5); /* Sombra escura para contraste */
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.masthead-heading {
|
||||||
|
color: #FFF8DC !important; /* Cornsilk - creme claro quente */
|
||||||
|
text-shadow:
|
||||||
|
4px 4px 10px rgba(0, 0, 0, 0.95),
|
||||||
|
-2px -2px 4px rgba(0, 0, 0, 0.8),
|
||||||
|
2px 2px 3px rgba(0, 0, 0, 0.6); /* Sombra escura forte */
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services icon colors - RED */
|
||||||
|
.fa-stack .text-primary {
|
||||||
|
color: var(--primary-red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar styling - LIGHT CREAM BACKGROUND */
|
||||||
|
#mainNav {
|
||||||
|
background-color: var(--light-bg) !important;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainNav .navbar-nav .nav-link {
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainNav .navbar-nav .nav-link:hover,
|
||||||
|
#mainNav .navbar-nav .nav-link:focus {
|
||||||
|
color: var(--primary-red) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainNav.navbar-shrink {
|
||||||
|
background-color: var(--cream) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar toggler - RED */
|
||||||
|
.navbar-toggler {
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler-icon {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(196, 33, 39, 1)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navbar brand hover */
|
||||||
|
.navbar-brand:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form control focus - RED */
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(196, 33, 39, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading animation for images */
|
||||||
|
img {
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tag buttons - RED THEME */
|
||||||
|
.btn-group-wrap .btn {
|
||||||
|
margin: 0.25rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-wrap .btn-primary {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-wrap .btn-outline-primary {
|
||||||
|
color: var(--primary-red);
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-wrap .btn-outline-primary:hover {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Portfolio caption styling */
|
||||||
|
.portfolio-caption {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-caption-heading {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #212529;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-caption-subheading {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section headings - RED accents */
|
||||||
|
.section-heading {
|
||||||
|
color: #212529;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 80px;
|
||||||
|
height: 3px;
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer styling */
|
||||||
|
.footer {
|
||||||
|
background-color: #212529;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #ffffff;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
color: var(--accent-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button social - RED */
|
||||||
|
.btn-social {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social:hover {
|
||||||
|
background-color: var(--dark-red);
|
||||||
|
border-color: var(--dark-red);
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.portfolio-hover-content p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-image i {
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainNav {
|
||||||
|
background-color: var(--cream) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scroll behavior */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Portfolio item image container */
|
||||||
|
.portfolio-item img,
|
||||||
|
.portfolio-item > a > div[style*="height"] {
|
||||||
|
width: 100%;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portfolio-item:hover img,
|
||||||
|
.portfolio-item:hover > a > div[style*="height"] {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
22
wwwroot/css/site.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||||
|
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
11316
wwwroot/css/styles.css
Normal file
BIN
wwwroot/favicon.ico
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
wwwroot/img/LogoPequeno.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
26
wwwroot/img/logo-optimized.svg
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
||||||
|
<!-- Carneiro Tech Logo - Optimized with Transparent Background -->
|
||||||
|
<!-- Red spiral/shell design -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="redGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#C42127;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#8B1E23;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Spiral shell design inspired by Carneiro logo -->
|
||||||
|
<path d="M100,20 Q150,30 160,80 Q165,130 130,160 Q90,180 60,150 Q40,110 60,70 Q80,40 100,20 Z"
|
||||||
|
fill="url(#redGradient)"
|
||||||
|
stroke="#8B1E23"
|
||||||
|
stroke-width="2"/>
|
||||||
|
|
||||||
|
<path d="M100,40 Q130,45 135,75 Q138,105 115,125 Q90,140 70,120 Q55,95 70,75 Q85,55 100,40 Z"
|
||||||
|
fill="#C42127"
|
||||||
|
opacity="0.8"/>
|
||||||
|
|
||||||
|
<path d="M100,60 Q115,63 118,80 Q120,97 108,108 Q95,118 83,107 Q73,93 83,80 Q93,68 100,60 Z"
|
||||||
|
fill="#E63946"/>
|
||||||
|
|
||||||
|
<!-- Center dot -->
|
||||||
|
<circle cx="100" cy="85" r="8" fill="#FFFFFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1008 B |
67
wwwroot/img/logo.svg
Normal file
|
After Width: | Height: | Size: 744 KiB |
54
wwwroot/js/scripts.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*!
|
||||||
|
* Start Bootstrap - Agency v7.0.12 (https://startbootstrap.com/theme/agency)
|
||||||
|
* Copyright 2013-2023 Start Bootstrap
|
||||||
|
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-agency/blob/master/LICENSE)
|
||||||
|
*/
|
||||||
|
//
|
||||||
|
// Scripts
|
||||||
|
//
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', event => {
|
||||||
|
|
||||||
|
// Navbar shrink function
|
||||||
|
var navbarShrink = function () {
|
||||||
|
const navbarCollapsible = document.body.querySelector('#mainNav');
|
||||||
|
if (!navbarCollapsible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (window.scrollY === 0) {
|
||||||
|
navbarCollapsible.classList.remove('navbar-shrink')
|
||||||
|
} else {
|
||||||
|
navbarCollapsible.classList.add('navbar-shrink')
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Shrink the navbar
|
||||||
|
navbarShrink();
|
||||||
|
|
||||||
|
// Shrink the navbar when page is scrolled
|
||||||
|
document.addEventListener('scroll', navbarShrink);
|
||||||
|
|
||||||
|
// Activate Bootstrap scrollspy on the main nav element
|
||||||
|
const mainNav = document.body.querySelector('#mainNav');
|
||||||
|
if (mainNav) {
|
||||||
|
new bootstrap.ScrollSpy(document.body, {
|
||||||
|
target: '#mainNav',
|
||||||
|
rootMargin: '0px 0px -40%',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collapse responsive navbar when toggler is visible
|
||||||
|
const navbarToggler = document.body.querySelector('.navbar-toggler');
|
||||||
|
const responsiveNavItems = [].slice.call(
|
||||||
|
document.querySelectorAll('#navbarResponsive .nav-link')
|
||||||
|
);
|
||||||
|
responsiveNavItems.map(function (responsiveNavItem) {
|
||||||
|
responsiveNavItem.addEventListener('click', () => {
|
||||||
|
if (window.getComputedStyle(navbarToggler).display !== 'none') {
|
||||||
|
navbarToggler.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
4
wwwroot/js/site.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
|
||||||
|
// for details on configuring this project to bundle and minify static web assets.
|
||||||
|
|
||||||
|
// Write your JavaScript code.
|
||||||
22
wwwroot/lib/bootstrap/LICENSE
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2011-2021 Twitter, Inc.
|
||||||
|
Copyright (c) 2011-2021 The Bootstrap Authors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
4997
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
1
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
7
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
1
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
4996
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
1
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
7
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
1
wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
427
wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2021 The Bootstrap Authors
|
||||||
|
* Copyright 2011-2021 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||||
|
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||||
|
*/
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
:root {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--bs-body-font-family);
|
||||||
|
font-size: var(--bs-body-font-size);
|
||||||
|
font-weight: var(--bs-body-font-weight);
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
color: var(--bs-body-color);
|
||||||
|
text-align: var(--bs-body-text-align);
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: inherit;
|
||||||
|
background-color: currentColor;
|
||||||
|
border: 0;
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr:not([size]) {
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, h5, h4, h3, h2, h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: calc(1.375rem + 1.5vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: calc(1.325rem + 0.9vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: calc(1.3rem + 0.6vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title],
|
||||||
|
abbr[data-bs-original-title] {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
cursor: help;
|
||||||
|
-webkit-text-decoration-skip-ink: none;
|
||||||
|
text-decoration-skip-ink: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
address {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol ol,
|
||||||
|
ul ul,
|
||||||
|
ol ul,
|
||||||
|
ul ol {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
padding: 0.2em;
|
||||||
|
background-color: #fcf8e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.75em;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0d6efd;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #0a58ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
direction: ltr /* rtl:ignore */;
|
||||||
|
unicode-bidi: bidi-override;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: #d63384;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
a > code {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
font-size: 0.875em;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #212529;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
kbd kbd {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
caption-side: bottom;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
text-align: inherit;
|
||||||
|
text-align: -webkit-match-parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
tr,
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
border-color: inherit;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus:not(:focus-visible) {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
button,
|
||||||
|
select,
|
||||||
|
optgroup,
|
||||||
|
textarea {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role=button] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
select:disabled {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[list]::-webkit-calendar-picker-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type=button],
|
||||||
|
[type=reset],
|
||||||
|
[type=submit] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[type=button]:not(:disabled),
|
||||||
|
[type=reset]:not(:disabled),
|
||||||
|
[type=submit]:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
float: left;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: calc(1.275rem + 0.3vw);
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
legend {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legend + * {
|
||||||
|
clear: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-datetime-edit-fields-wrapper,
|
||||||
|
::-webkit-datetime-edit-text,
|
||||||
|
::-webkit-datetime-edit-minute,
|
||||||
|
::-webkit-datetime-edit-hour-field,
|
||||||
|
::-webkit-datetime-edit-day-field,
|
||||||
|
::-webkit-datetime-edit-month-field,
|
||||||
|
::-webkit-datetime-edit-year-field {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=search] {
|
||||||
|
outline-offset: -2px;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rtl:raw:
|
||||||
|
[type="tel"],
|
||||||
|
[type="url"],
|
||||||
|
[type="email"],
|
||||||
|
[type="number"] {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-color-swatch-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::file-selector-button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
font: inherit;
|
||||||
|
-webkit-appearance: button;
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||||