2024-11-21

3 Days from Mockup to Production: Real CRM Implementation

Client request: "We need a CRM module. Contact management, deal pipeline, task tracking. How long?"

Traditional answer: "3 months with 4 developers."

PMFA answer: "3 days with 1 developer."

Client: "Impossible."

This blog post: Receipts.


The Challenge

Requirements

Module: CRM (Customer Relationship Management)

Entities:

  1. Contact - customers, leads, prospects
  2. Company - organizations contacts belong to
  3. Deal - sales opportunities with pipeline stages
  4. Task - to-do items assigned to users
  5. Activity - calls, meetings, emails (audit trail)

Features:

  • Contact management (CRUD + search + import)
  • Deal pipeline (drag-drop between stages)
  • Task assignment & notifications
  • Activity timeline per contact
  • Dashboard (deals by stage, tasks by user)
  • Email integration (send/receive)

Expected timeline (traditional):

  • Backend: 6 weeks
  • Frontend: 4 weeks
  • Integration: 2 weeks
  • Testing: 2 weeks
  • Total: 3 months

PMFA timeline: 3 days. Let's see how.


Day 1 (Friday Evening): Define Metadata

Step 1: Entity Definitions (YAML)

# meta_model.yaml

Company:
  fields:
    - name:
        type: string
        required: true
    
    - industry:
        type: enum
        values: [technology, finance, retail, manufacturing, other]
    
    - website:
        type: url
    
    - employee_count:
        type: integer
        validation: "employee_count > 0"
  
  relations:
    - contacts:
        type: has_many(Contact)

Contact:
  fields:
    - first_name:
        type: string
        required: true
    
    - last_name:
        type: string
        required: true
    
    - email:
        type: email
        required: true
        unique: true
    
    - phone:
        type: phone
    
    - title:
        type: string  # Job title
    
    - company_id:
        type: reference(Company)
    
    - status:
        type: enum
        values: [lead, prospect, customer, inactive]
        default: lead
  
  relations:
    - company:
        type: belongs_to(Company)
    
    - deals:
        type: has_many(Deal)
    
    - tasks:
        type: has_many(Task)
    
    - activities:
        type: has_many(Activity)
  
  ui:
    list:
      columns: [first_name, last_name, email, company.name, status]
      filters: [status, company_id]
      search: [first_name, last_name, email]
    
    form:
      layout: two_column
      sections:
        - title: "Contact Info"
          fields: [first_name, last_name, email, phone]
        - title: "Company"
          fields: [company_id, title]
        - title: "Status"
          fields: [status]

Deal:
  fields:
    - title:
        type: string
        required: true
    
    - contact_id:
        type: reference(Contact)
        required: true
    
    - amount:
        type: decimal
        required: true
        validation: "amount > 0"
    
    - probability:
        type: integer
        validation: "probability >= 0 AND probability <= 100"
        default: 50
    
    - stage:
        type: enum
        values: [lead, qualified, proposal, negotiation, won, lost]
        default: lead
    
    - expected_close_date:
        type: date
    
    - assigned_to:
        type: reference(User)
  
  workflow:
    states:
      - lead:
          transitions:
            - to: qualified
              action: qualify
              conditions: ["probability >= 20"]
            - to: lost
              action: mark_lost
      
      - qualified:
          transitions:
            - to: proposal
              action: send_proposal
            - to: lost
              action: mark_lost
      
      - proposal:
          transitions:
            - to: negotiation
              action: start_negotiation
            - to: lost
              action: mark_lost
      
      - negotiation:
          transitions:
            - to: won
              action: close_won
              conditions: ["amount > 0", "expected_close_date IS NOT NULL"]
            - to: lost
              action: mark_lost
      
      - won:
          final: true
          on_enter:
            - trigger: create_invoice
            - notify: finance_team
      
      - lost:
          final: true
          requires_reason: true
  
  ui:
    kanban:
      group_by: stage
      card_fields: [title, contact.name, amount, probability]
    
    list:
      columns: [title, contact.name, amount, stage, expected_close_date]
      filters: [stage, assigned_to, date_range]

Task:
  fields:
    - title:
        type: string
        required: true
    
    - description:
        type: text
    
    - due_date:
        type: datetime
        required: true
    
    - priority:
        type: enum
        values: [low, medium, high, urgent]
        default: medium
    
    - status:
        type: enum
        values: [pending, in_progress, done, cancelled]
        default: pending
    
    - assigned_to:
        type: reference(User)
        required: true
    
    - related_to:
        type: polymorphic[Contact, Deal]
  
  workflow:
    states:
      - pending:
          transitions:
            - to: in_progress
              action: start
      
      - in_progress:
          transitions:
            - to: done
              action: complete
            - to: cancelled
              action: cancel
      
      - done:
          final: true
      
      - cancelled:
          final: true
  
  notifications:
    - event: due_date_approaching
      trigger: "due_date - NOW() < '24 hours'"
      recipients: [assigned_to]
      template: task_reminder

Activity:
  fields:
    - type:
        type: enum
        values: [call, meeting, email, note]
    
    - subject:
        type: string
        required: true
    
    - description:
        type: text
    
    - contact_id:
        type: reference(Contact)
        required: true
    
    - created_by:
        type: reference(User)
    
    - scheduled_at:
        type: datetime
  
  ui:
    timeline:
      order_by: created_at DESC
      group_by: date

Time spent: 2 hours (writing YAML)

Lines: ~250 lines of metadata


Day 2 (Saturday Morning): Code Generation

Step 2: Generate Backend + Frontend

$ pmfa generate --from meta_model.yaml --output src/

Analyzing metadata...
✅ Found 5 entities: Company, Contact, Deal, Task, Activity

Generating database schema...
✅ Created 12 tables (including junction tables)
✅ Created 24 indexes
✅ Created 8 foreign keys

Generating TypeScript types...
✅ Generated 45 interfaces
✅ Generated 15 enums
✅ Generated 25 DTOs (Data Transfer Objects)

Generating REST API...
✅ Generated 60 API endpoints:
   - GET    /api/contacts
   - POST   /api/contacts
   - GET    /api/contacts/:id
   - PUT    /api/contacts/:id
   - DELETE /api/contacts/:id
   - POST   /api/deals/:id/qualify
   - POST   /api/deals/:id/send-proposal
   ... (54 more)

Generating validation logic...
✅ Generated 32 validators
✅ Generated 18 business rules

Generating workflow engines...
✅ Generated 3 state machines (Deal, Task, Activity)

Generating UI components...
✅ Generated 15 React forms
✅ Generated 8 list views
✅ Generated 1 Kanban board (Deal pipeline)
✅ Generated 1 Timeline component (Activity)

Generating tests...
✅ Generated 127 unit tests
✅ Generated 45 integration tests

Total time: 23 seconds

What PMFA generated:

  • 12 database tables (PostgreSQL)
  • 45 TypeScript interfaces
  • 60 REST API endpoints (Express.js)
  • 15 React components (forms, lists, dashboards)
  • 3 workflow engines
  • 127 unit tests

Time spent: 23 seconds (automatic)

Step 3: Deploy to Staging

$ pmfa deploy --environment staging

Building...
✅ Compiled TypeScript (8 seconds)
✅ Built React frontend (12 seconds)
✅ Generated Docker image (15 seconds)

Deploying to Kubernetes...
✅ Applied database migrations (3 seconds)
✅ Deployed backend pods (8 seconds)
✅ Deployed frontend (5 seconds)

Staging URL: https://crm-staging.example.com

Total time: 51 seconds

Result: Fully functional CRM running on staging environment.

Time spent: 51 seconds (automatic)


Day 2 (Saturday Afternoon): Custom Logic

Step 4: Add Business-Specific Requirements

Client: "When a deal is won, we need to:"

  1. Create an invoice automatically
  2. Notify finance team
  3. Update contact status to 'customer'

PMFA custom logic:

// src/custom/deal_hooks.ts

import { PMFA } from '@pmfa/runtime';

// Hook: Deal state change
PMFA.registerHook('Deal.onStateChange', async (deal, oldStage, newStage) => {
  if (newStage === 'won') {
    // 1. Create invoice
    const invoice = await PMFA.entities.Invoice.create({
      deal_id: deal.id,
      contact_id: deal.contact_id,
      amount: deal.amount,
      due_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
      status: 'draft'
    });
    
    // 2. Notify finance team
    await PMFA.notifications.send({
      to: 'finance@company.com',
      template: 'new_invoice_created',
      data: { invoice, deal }
    });
    
    // 3. Update contact status
    await PMFA.entities.Contact.update(deal.contact_id, {
      status: 'customer'
    });
  }
});

// Validator: Amount must match contact's budget
PMFA.registerValidator('Deal.beforeCreate', async (deal) => {
  const contact = await PMFA.entities.Contact.findById(deal.contact_id);
  
  if (deal.amount > contact.approved_budget) {
    throw new PMFA.ValidationError(
      `Deal amount ($${deal.amount}) exceeds approved budget ($${contact.approved_budget})`
    );
  }
});

Time spent: 1 hour (writing custom logic)

Lines: 50 lines of TypeScript


Day 3 (Sunday): Testing & Polish

Step 5: Run Tests

$ pmfa test

Running unit tests...
✅ 127/127 passed (auto-generated)

Running custom tests...
✅ 12/12 passed

Running integration tests...
✅ 45/45 passed

Code coverage: 94%

Total time: 2 minutes 15 seconds

Time spent: 2 minutes (automatic)

Step 6: Import Sample Data

$ pmfa import --from sample_data.csv

Importing contacts...
✅ Created 150 contacts

Importing companies...
✅ Created 50 companies

Importing deals...
✅ Created 80 deals

Total time: 12 seconds

Time spent: 12 seconds (automatic)

Step 7: Deploy to Production

$ pmfa deploy --environment production

Building...
✅ Compiled TypeScript
✅ Built React frontend
✅ Optimized assets

Deploying...
✅ Database migrations applied (0 downtime)
✅ Backend deployed (rolling update)
✅ Frontend deployed (CDN cached)

Production URL: https://crm.example.com

Monitoring:
  ✅ Health check: OK
  ✅ API latency: 45ms (avg)
  ✅ Frontend load time: 1.2s

Total time: 3 minutes 12 seconds

Time spent: 3 minutes (automatic)


Final Timeline

Day Task Time
Day 1 (Friday) Define YAML metadata 2 hours
Day 2 (Saturday AM) Generate code + deploy staging 23 sec + 51 sec
Day 2 (Saturday PM) Write custom business logic 1 hour
Day 3 (Sunday) Testing + import data + deploy prod 5 min
Total 3 hours 6 minutes

Developer-days: 0.4 (one developer, 3 hours spread over 3 days)


Comparison: PMFA vs Traditional

Traditional Framework (React + Node.js + TypeScript)

Backend development:

// Just ONE entity (Contact) requires:

// 1. Database migration (20 lines)
CREATE TABLE contacts (
  id SERIAL PRIMARY KEY,
  first_name VARCHAR(100),
  last_name VARCHAR(100),
  email VARCHAR(255) UNIQUE,
  -- ... 10 more fields
);

// 2. TypeScript entity (40 lines)
interface Contact {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  // ... 10 more fields
}

// 3. Repository (80 lines)
class ContactRepository {
  async findAll() { /* ... */ }
  async findById(id: number) { /* ... */ }
  async create(data: CreateContactDto) { /* ... */ }
  async update(id: number, data: UpdateContactDto) { /* ... */ }
  async delete(id: number) { /* ... */ }
}

// 4. Service layer (120 lines)
class ContactService {
  constructor(private repo: ContactRepository) {}
  
  async create(data: CreateContactDto) {
    // Validation
    if (!data.email) throw new Error('Email required');
    if (!isValidEmail(data.email)) throw new Error('Invalid email');
    
    // Check duplicates
    const existing = await this.repo.findByEmail(data.email);
    if (existing) throw new Error('Email already exists');
    
    // Create
    return this.repo.create(data);
  }
  
  // ... 80 more lines for update, delete, search, etc.
}

// 5. REST API controller (60 lines)
@Controller('/api/contacts')
class ContactController {
  @Get('/')
  async list() { /* ... */ }
  
  @Post('/')
  async create(@Body() dto: CreateContactDto) { /* ... */ }
  
  // ... 40 more lines
}

// 6. DTOs (30 lines)
class CreateContactDto {
  @IsString() firstName: string;
  @IsEmail() email: string;
  // ... validation decorators
}

// 7. Unit tests (150 lines)
describe('ContactService', () => {
  it('should create contact', async () => { /* ... */ });
  it('should reject duplicate email', async () => { /* ... */ });
  // ... 20 more tests
});

Frontend development:

// Contact form component (200 lines)
function ContactForm({ contactId }: Props) {
  const [contact, setContact] = useState<Contact>({});
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (contactId) {
      fetchContact(contactId);
    }
  }, [contactId]);
  
  const fetchContact = async (id: number) => {
    setLoading(true);
    try {
      const data = await api.get(`/contacts/${id}`);
      setContact(data);
    } catch (err) {
      setErrors({ general: err.message });
    } finally {
      setLoading(false);
    }
  };
  
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    
    // Client-side validation
    const newErrors: Record<string, string> = {};
    if (!contact.firstName) newErrors.firstName = 'Required';
    if (!contact.email) newErrors.email = 'Required';
    if (contact.email && !isValidEmail(contact.email)) {
      newErrors.email = 'Invalid email';
    }
    
    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }
    
    // Submit
    setLoading(true);
    try {
      if (contactId) {
        await api.put(`/contacts/${contactId}`, contact);
      } else {
        await api.post('/contacts', contact);
      }
      navigate('/contacts');
    } catch (err) {
      setErrors({ general: err.message });
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        name="firstName"
        value={contact.firstName || ''}
        onChange={(e) => setContact({ ...contact, firstName: e.target.value })}
      />
      {errors.firstName && <span className="error">{errors.firstName}</span>}
      
      {/* ... 150 more lines for other fields */}
    </form>
  );
}

Total for ONE entity (Contact):

  • Backend: 500 lines
  • Frontend: 200 lines
  • Tests: 150 lines
  • Total: 850 lines

CRM has 5 entities: 850 × 5 = 4,250 lines minimum

Plus:

  • Workflow engines (500 lines)
  • Dashboard (300 lines)
  • Integration logic (400 lines)
  • Grand total: ~5,500 lines

Time estimate:

  • 1 developer: 3 months
  • 2 developers: 6-8 weeks
  • 4 developers: 4-6 weeks

PMFA Approach

Metadata: 250 lines of YAML
Custom logic: 50 lines of TypeScript
Total: 300 lines

Generated automatically:

  • 12 database tables
  • 60 API endpoints
  • 15 React components
  • 127 unit tests
  • 3 workflow engines

Time: 3 hours over 3 days


Key Insights

Why PMFA is 30x Faster

  1. No boilerplate code - PMFA generates standard CRUD operations
  2. No manual testing - Auto-generated tests cover 80% of logic
  3. No deployment scripts - PMFA handles database migrations + rolling updates
  4. No UI development - Forms/lists generated from metadata

What You Still Write

Custom business logic only:

  • Unique validations (20% of rules)
  • Integration with external systems
  • Complex calculations
  • Business-specific workflows

Result: You write 300 lines instead of 5,500 lines.


Conclusion

Traditional framework:

  • ❌ 5,500 lines of code
  • ❌ 3 months with 4 developers
  • ❌ $150K development cost

PMFA platform:

  • ✅ 300 lines (250 YAML + 50 TypeScript)
  • ✅ 3 days with 1 developer
  • ✅ $5K development cost

Savings: $145K + 2.5 months faster to market

The secret: PMFA treats entities, workflows, and UI as declarative metadata, not imperative code. Generate once, deploy everywhere.

Want to build ERP modules in days instead of months? Contact us at office@nonnotech.com for PMFA technical demo.