Company Service
The Company Service manages company profiles, job postings, and interactions with students. It provides robust validation of company information using official French government APIs to ensure that only legitimate businesses can use the platform.
Features
- Company profile management
- Job posting creation and management
- Applicant tracking
- Interview scheduling
- Analytics and reporting
- SIRET validation using French government APIs
- Company verification process
Technical Implementation
Company Verification Flow
The company verification flow in RovesUp follows these steps:
- Company registers with basic information including SIRET number
- System validates the SIRET number using the French government API
- If valid, company profile is created with verified status
- Company can then create job postings and interact with students
SIRET Validation
We use the official French government API (API Entreprise) to validate SIRET numbers and retrieve company information. This ensures that only legitimate businesses can register on the platform.
import axios from 'axios';
import { logger } from '../utils/logger';
// API Entreprise configuration
const API_ENTREPRISE_BASE_URL = 'https://entreprise.api.gouv.fr/v3';
const API_ENTREPRISE_TOKEN = process.env.API_ENTREPRISE_TOKEN;
interface SiretValidationResult {
isValid: boolean;
companyData?: {
name: string;
address: string;
activityCode: string;
legalForm: string;
creationDate: string;
};
error?: string;
}
/**
* Validates a SIRET number using the French government API
* @param siret The SIRET number to validate
* @returns Validation result with company data if valid
*/
export async function validateSiret(siret: string): Promise<SiretValidationResult> {
try {
// Remove any spaces or dashes
const cleanSiret = siret.replace(/\s|-/g, '');
// Check if the SIRET has the correct length
if (cleanSiret.length !== 14) {
return {
isValid: false,
error: 'SIRET must be 14 digits'
};
}
// Check if the SIRET contains only digits
if (!/^\d+$/.test(cleanSiret)) {
return {
isValid: false,
error: 'SIRET must contain only digits'
};
}
// Call the API Entreprise to validate the SIRET
const response = await axios.get(
`${API_ENTREPRISE_BASE_URL}/etablissements/${cleanSiret}`,
{
headers: {
'Authorization': `Bearer ${API_ENTREPRISE_TOKEN}`,
'Accept': 'application/json'
}
}
);
// Check if the API returned a valid result
if (response.status === 200 && response.data.etablissement) {
const etablissement = response.data.etablissement;
const entreprise = response.data.entreprise;
// Check if the establishment is active
if (etablissement.etat_administratif !== 'A') {
return {
isValid: false,
error: 'Company is not active'
};
}
// Return the company data
return {
isValid: true,
companyData: {
name: entreprise.raison_sociale || '',
address: formatAddress(etablissement.adresse),
activityCode: etablissement.naf || '',
legalForm: entreprise.forme_juridique?.libelle || '',
creationDate: entreprise.date_creation || ''
}
};
}
return {
isValid: false,
error: 'SIRET not found or invalid'
};
} catch (error) {
logger.error('Error validating SIRET', error);
// Handle specific API error responses
if (axios.isAxiosError(error) && error.response) {
if (error.response.status === 404) {
return {
isValid: false,
error: 'SIRET not found'
};
}
if (error.response.status === 403) {
return {
isValid: false,
error: 'API access forbidden'
};
}
}
return {
isValid: false,
error: 'Error validating SIRET'
};
}
}
/**
* Formats an address from the API response
* @param addressData Address data from the API
* @returns Formatted address string
*/
function formatAddress(addressData: any): string {
const components = [
addressData.numero_voie,
addressData.type_voie,
addressData.libelle_voie,
addressData.complement_adresse,
addressData.code_postal,
addressData.libelle_commune
].filter(Boolean);
return components.join(' ');
}
/**
* Example usage in company registration
*/
export async function registerCompany(req: Request, res: Response): Promise<void> {
try {
const { siret, email, password, contactName, phoneNumber } = req.body;
// Validate input
if (!siret || !email || !password || !contactName || !phoneNumber) {
return res.status(400).json({ error: 'Missing required fields' });
}
// Validate SIRET
const siretValidation = await validateSiret(siret);
if (!siretValidation.isValid) {
return res.status(400).json({
error: 'Invalid SIRET',
details: siretValidation.error
});
}
// Check if company already exists
const existingCompany = await CompanyModel.findOne({ siret: siret.replace(/\s|-/g, '') });
if (existingCompany) {
return res.status(409).json({ error: 'Company already registered' });
}
// Create user account for company
const userResponse = await axios.post(
`${process.env.AUTH_SERVICE_URL}/auth/register`,
{
email,
password,
userType: 'company'
}
);
// Create company profile
const company = new CompanyModel({
userId: userResponse.data.user._id,
siret: siret.replace(/\s|-/g, ''),
name: siretValidation.companyData.name,
address: siretValidation.companyData.address,
activityCode: siretValidation.companyData.activityCode,
legalForm: siretValidation.companyData.legalForm,
creationDate: new Date(siretValidation.companyData.creationDate),
contactName,
contactEmail: email,
contactPhone: phoneNumber,
isVerified: true,
verificationDate: new Date(),
createdAt: new Date(),
updatedAt: new Date()
});
await company.save();
// Return the company profile and token
res.status(201).json({
token: userResponse.data.token,
company: {
id: company._id,
name: company.name,
isVerified: company.isVerified
}
});
} catch (error) {
logger.error('Error registering company', error);
res.status(500).json({ error: 'Registration failed' });
}
}
SIRET Validation Luhn Algorithm
In addition to API validation, we also perform a local check using the Luhn algorithm to validate SIRET numbers:
/**
* Validates a SIRET number using the Luhn algorithm
* @param siret The SIRET number to validate
* @returns Whether the SIRET is valid according to the Luhn algorithm
*/
export function validateSiretWithLuhn(siret: string): boolean {
// Remove any spaces or dashes
const cleanSiret = siret.replace(/\s|-/g, '');
// Check if the SIRET has the correct length
if (cleanSiret.length !== 14) {
return false;
}
// Check if the SIRET contains only digits
if (!/^\d+$/.test(cleanSiret)) {
return false;
}
// Apply the Luhn algorithm
let sum = 0;
let alternate = false;
for (let i = cleanSiret.length - 1; i >= 0; i--) {
let n = parseInt(cleanSiret.charAt(i), 10);
if (alternate) {
n *= 2;
if (n > 9) {
n = (n % 10) + 1;
}
}
sum += n;
alternate = !alternate;
}
return (sum % 10 === 0);
}
Database Schema
The Company Service uses the following MongoDB schema for company management:
import mongoose, { Schema, Document } from 'mongoose';
export interface ICompany extends Document {
userId: mongoose.Types.ObjectId;
siret: string;
name: string;
address: string;
activityCode: string;
legalForm: string;
creationDate: Date;
contactName: string;
contactEmail: string;
contactPhone: string;
website?: string;
description?: string;
logo?: string;
socialMedia?: {
linkedin?: string;
twitter?: string;
facebook?: string;
};
isVerified: boolean;
verificationDate?: Date;
createdAt: Date;
updatedAt: Date;
}
const CompanySchema: Schema = new Schema({
userId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
siret: { type: String, required: true, unique: true },
name: { type: String, required: true },
address: { type: String, required: true },
activityCode: { type: String, required: true },
legalForm: { type: String, required: true },
creationDate: { type: Date, required: true },
contactName: { type: String, required: true },
contactEmail: { type: String, required: true },
contactPhone: { type: String, required: true },
website: { type: String },
description: { type: String },
logo: { type: String },
socialMedia: {
linkedin: { type: String },
twitter: { type: String },
facebook: { type: String }
},
isVerified: { type: Boolean, default: false },
verificationDate: { type: Date },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// Pre-save hook to update the updatedAt field
CompanySchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
export const CompanyModel = mongoose.model<ICompany>('Company', CompanySchema);
Job Posting Schema
import mongoose, { Schema, Document } from 'mongoose';
export interface IJobPosting extends Document {
companyId: mongoose.Types.ObjectId;
title: string;
description: string;
requirements: string[];
location: {
city: string;
postalCode: string;
country: string;
remote: boolean;
};
contractType: 'CDI' | 'CDD' | 'Internship' | 'Apprenticeship' | 'Freelance';
salary: {
min?: number;
max?: number;
currency: string;
period: 'hourly' | 'monthly' | 'yearly';
negotiable: boolean;
};
startDate?: Date;
endDate?: Date;
applicationDeadline?: Date;
isActive: boolean;
views: number;
applications: number;
createdAt: Date;
updatedAt: Date;
}
const JobPostingSchema: Schema = new Schema({
companyId: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'Company' },
title: { type: String, required: true },
description: { type: String, required: true },
requirements: [{ type: String }],
location: {
city: { type: String, required: true },
postalCode: { type: String, required: true },
country: { type: String, required: true, default: 'France' },
remote: { type: Boolean, default: false }
},
contractType: {
type: String,
required: true,
enum: ['CDI', 'CDD', 'Internship', 'Apprenticeship', 'Freelance']
},
salary: {
min: { type: Number },
max: { type: Number },
currency: { type: String, default: 'EUR' },
period: { type: String, enum: ['hourly', 'monthly', 'yearly'], default: 'monthly' },
negotiable: { type: Boolean, default: false }
},
startDate: { type: Date },
endDate: { type: Date },
applicationDeadline: { type: Date },
isActive: { type: Boolean, default: true },
views: { type: Number, default: 0 },
applications: { type: Number, default: 0 },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now }
});
// Pre-save hook to update the updatedAt field
JobPostingSchema.pre('save', function(next) {
this.updatedAt = new Date();
next();
});
export const JobPostingModel = mongoose.model<IJobPosting>('JobPosting', JobPostingSchema);
API Endpoints
Company Management
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/companies | POST | Register a new company | No |
/api/companies/verify | POST | Verify a company's SIRET | No |
/api/companies/profile | GET | Get company profile | Yes (Company) |
/api/companies/profile | PUT | Update company profile | Yes (Company) |
/api/companies/{id} | GET | Get public company details | No |
Job Posting Management
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/companies/jobs | POST | Create a new job posting | Yes (Company) |
/api/companies/jobs | GET | List company's job postings | Yes (Company) |
/api/companies/jobs/{id} | GET | Get job posting details | Yes (Company) |
/api/companies/jobs/{id} | PUT | Update job posting | Yes (Company) |
/api/companies/jobs/{id} | DELETE | Delete job posting | Yes (Company) |
/api/companies/jobs/{id}/activate | PUT | Activate job posting | Yes (Company) |
/api/companies/jobs/{id}/deactivate | PUT | Deactivate job posting | Yes (Company) |
/api/companies/jobs/{id}/applications | GET | List applications for a job | Yes (Company) |
Application Management
| Endpoint | Method | Description | Authentication Required |
|---|---|---|---|
/api/companies/applications/{id} | GET | Get application details | Yes (Company) |
/api/companies/applications/{id}/status | PUT | Update application status | Yes (Company) |
/api/companies/applications/{id}/schedule-interview | POST | Schedule an interview | Yes (Company) |
Integration with French Government APIs
API Entreprise
We use the API Entreprise provided by the French government to validate company information. This API requires an API key that can be obtained by registering on the API Entreprise website.
Required Permissions
To use the API Entreprise for SIRET validation, the following permissions are required:
entreprises- Access to company informationetablissements- Access to establishment information
Rate Limiting
The API Entreprise has rate limiting in place:
- 2000 requests per 10 minutes
- 5000 requests per day
Our implementation includes caching and rate limiting to ensure we stay within these limits.
Alternative APIs
In case the API Entreprise is unavailable, we have fallback options:
- INSEE API - Provides similar company information
- Infogreffe API - Commercial register information
Security Considerations
- All company data is validated against official government sources
- SIRET numbers are validated using both API calls and the Luhn algorithm
- Company verification status is clearly indicated to students
- Only verified companies can post jobs
- All API calls to government services are made server-side to protect API keys
Error Handling
The Company Service uses consistent error handling:
// Error response structure
interface ErrorResponse {
error: string;
details?: any;
code?: string;
}
// Common error handling middleware
export function errorHandler(err: any, req: Request, res: Response, next: NextFunction): void {
logger.error('Error occurred', err);
// Handle specific error types
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation error',
details: err.errors
});
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({
error: 'Authentication error',
code: 'UNAUTHORIZED'
});
}
// Default error response
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
Integration with Other Services
The Company Service integrates with other RovesUp services:
- Authentication Service: For company user management
- Student Service: For accessing student applications
- Notification Service: For sending notifications about job applications
- AI Service: For job matching and recommendation
Deployment
The Company Service is deployed as a Docker container in a Kubernetes cluster:
# Kubernetes deployment configuration
apiVersion: apps/v1
kind: Deployment
metadata:
name: company-service
namespace: rovesup
spec:
replicas: 3
selector:
matchLabels:
app: company-service
template:
metadata:
labels:
app: company-service
spec:
containers:
- name: company-service
image: registry.gitlab.com/rovesup/company-service:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: MONGODB_URI
valueFrom:
secretKeyRef:
name: company-secrets
key: mongodb-uri
- name: API_ENTREPRISE_TOKEN
valueFrom:
secretKeyRef:
name: company-secrets
key: api-entreprise-token
- name: AUTH_SERVICE_URL
value: "http://auth-service:3000"
- name: STUDENT_SERVICE_URL
value: "http://student-service:3000"
- name: NOTIFICATION_SERVICE_URL
value: "http://notification-service:3000"
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "200m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10